diff --git a/scripts/__baseline__/README.md b/scripts/__baseline__/README.md index 28ddb66..bfeb53b 100644 --- a/scripts/__baseline__/README.md +++ b/scripts/__baseline__/README.md @@ -5,33 +5,20 @@ agent layer existed. `skill-check.mjs` asserts against it on every run. Regenerating it is a deliberate act, and the reason belongs here. -## OUTSTANDING — the HTML baselines contain data that no longer exists +## 2026-09-20 — the three HTML baselines that held invented people were recaptured -**Six checks fail on purpose. Do not regenerate these baselines to clear them.** - - Hired History paints the same styled elements, in the same order - Hired History shows the same words - Hired History added only identity wrappers - analytics: paints the same styled elements, in the same order - analytics: shows the same words - candidates: shows the same words - -The first five are the `DEMO_FILL` removal, described immediately below. The -sixth is the final-selection queue and has its own dated entry further down. +Settled. The section that stood here said six checks failed on purpose and +listed the evidence; this is that debt being paid. `hiringRecords.js` used to pad the hires list with five invented people (`DEMO_FILL`) so Hired History read as a history rather than as three rows. The -padding applied to Analytics too, so "total hires" counted eight where the -database held three. It has been removed: hires now come only from `staff`. +padding reached Analytics too, where "total hires" counted eight against a +database holding three. It was removed, and because these baselines render with +queries disabled, the padding was *all* they contained. Candidates separately +became the final-selection queue, which changed what its empty state says. -`hired-history.pre-migration.html` and `analytics.pre-migration.html` were -captured **while the padding was in effect** — and because these baselines render -with queries disabled, the padding is *all* they contain. The Hired History -baseline is 31,453 characters holding all five invented names; the genuine empty -state is 11,947 and holds none. - -**Verified before leaving them failing**, so the drift is known rather than -assumed. Tag counts, baseline → now: +Verified before regenerating, so the drift was known rather than assumed. Tag +counts for Hired History, baseline -> now: 0 five table rows of people who were never hired 13 @@ -39,25 +26,57 @@ assumed. Tag counts, baseline → now:

7 8 -Every difference is content that was fabricated. No styling, ordering or -structural rule changed. +Every difference was content that had been fabricated. Analytics lost 269 class +attributes and 238 words to the same cause. Candidates gained exactly one class +— the description paragraph under "Nobody is awaiting a decision". No styling +rule, ordering rule or structural rule changed on any of the three. -**Why they are not regenerated yet.** Two of them — `added only identity -wrappers` and `carries node identity in the DOM` — exist to prove the UI node -tree migration added exactly two `

`s and nothing else. That proof needs the -baseline to be *pre*-migration markup. Recapturing now would write *post*- -migration markup into a file named `pre-migration`, and the check would compare -the current render against itself and pass forever without proving anything. +**Recaptured and renamed**, because a file called `pre-migration` that holds +post-`dca1842` markup is a lie in the filename: -So the debt is held until the migration work lands, at which point both files -are recaptured together and this section is replaced by a dated entry saying so. + hired-history.pre-migration.html -> hired-history.render.html + analytics.pre-migration.html -> analytics.render.html + candidates.pre-migration.html -> candidates.render.html + +`activity-page`, `candidates-analysis`, `control-center`, `positions` and +`talent-pool` still hold genuine pre-migration markup, still pass, and keep the +name that says so. The `PAGES` table in `skill-check.mjs` now carries each +baseline's FILENAME rather than deriving one suffix for all of them. + +**The migration proof was not thrown away with the baseline.** One check — +`Hired History added only identity wrappers` — compared tag tallies to assert +that migrating the page added exactly two `
`s and changed nothing else. +That was true, and it was checkable only while the DATA was frozen as well: the +predicate subtracts one whole render from another, so removing the invented +hires moved every count and the arithmetic stopped describing wrappers at all. +Recapturing would not have rescued it — with the baseline equal to the render +the delta is zero, and a predicate demanding two can never hold again. Left in +place it would have stayed red for a new reason, which is worse than failing +for the old one. + +It is replaced by three checks that read the render itself and need no frozen +file, so they keep holding as the page's content changes: + + Hired History wraps exactly the node types registered to wrap + Hired History identities are unique and name composed nodes + Hired History identity wrappers carry no styling + +Together these say what the tally said — `UiTreeRenderer` encloses a type +registered `wrap: true` in `
`, every other type takes the +attributes on its own root element, and a wrapper contributes identity and no +styling. Hired History composes five nodes; two are registered to wrap and +produce the two divs, two carry their identity on their own `
`, and +`hired-extensions-top` is an extension slot that renders nothing while no skill +is attached to it. The second check therefore asserts uniqueness and no strays +rather than one identity per composed node — a one-to-one rule would be +asserting that every extension slot is always filled. + +`carries node identity in the DOM` was described here as needing pre-migration +markup. It does not: it reads only the live render, names `chronology` and +`records` directly, and is unaffected by any of this. It still passes. + +`npm test` is 1693/1693. -**One related fix was made rather than deferred.** `HiredChronology` used to -`return null` with no hires, which took the `chronology` node identity out of the -DOM with it — so with an honest empty dataset the section could not be addressed -by Owliver or the layout editor at all, and the page said nothing where it most -needed to. It now renders an empty state inside the section, which the section -keeps. `carries node identity in the DOM` passes again because of it. ## 2026-08-27 — the seed gained the three statuses nothing exercised @@ -156,8 +175,9 @@ The new stage filter options cost nothing here. `FilterSelect` is a Radix `SelectValue` renders empty on the server — the baseline contains neither the old option labels nor the new ones. -**Why it is not regenerated.** The same reason as the five above: -`candidates.pre-migration.html` is load-bearing for the UI node tree proof, and -recapturing it now would write post-migration markup into a file named -`pre-migration`. It is held until the migration work lands and all of these are -recaptured together. +**Regenerated on 2026-09-20**, with the other two — see the entry at the top of +this file. It was held until then because `candidates.pre-migration.html` was +load-bearing for the UI node tree proof, and recapturing it earlier would have +written post-migration markup into a file named `pre-migration`. The file is now +`candidates.render.html` and the proof it was holding up has been replaced by +three checks that read the render directly. diff --git a/scripts/__baseline__/analytics.pre-migration.html b/scripts/__baseline__/analytics.pre-migration.html deleted file mode 100644 index 6fc7555..0000000 --- a/scripts/__baseline__/analytics.pre-migration.html +++ /dev/null @@ -1 +0,0 @@ -

Analytics

Live System

How hiring is performing — rates, trends, comparison and what to act on.

Hiring performance

Across the workspace

Total hires

5

Avg time-to-hire

2d
2d median

Quality of hire

90
avg AI score

Conversion rate

0%
0 of 0 applicants

Hiring funnel

Applied → Screened → Shortlisted → Interview → Hired

No applications on file yet. The funnel appears once the first candidate applies.

Hiring trend

Cumulative hires by month

Not enough history for a trend

All 5 hires closed in Jul. A month-on-month line appears once hiring spans a second month.

Department performance

4 departments compared

Department Performance & Hiring Flow

Interactive pipeline mapping AI score quality & hire velocity across departments

Top Department

Security (95)

Active Units

4 Departments

Overall AI Index

91 / 100

Top Fill Velocity

1d Avg

Pipeline Source

Talent Engine

100% AI Verified Engine

Total Hires

5 staff filled

Avg AI Score

91 / 100

Hiring Outcomes

Verified Quality Impact

100% Active Placement

Quality Tier

95+ Top

Placement

100% Verified

Avg Rating

4.9 / 5.0

Department Performance Units (4)

Click any department card for detailed staff roster
#1
Security
#1 Leader
1 hire•1d fill velocity
95
#2
Event Manager
1 hire•3d fill velocity
91
#3
Front Desk
2 hires•2d fill velocity
89
#4
Housekeeping
1 hire•2d fill velocity
87

Position performance

5 roles filled
Hires, quality, speed and review outcome by role
RoleHiresAvg scoreAvg daysReviewedRating
Guest Relations Lead1902d0/1
Pending
Event Coordinator1913d0/1
Pending
Operations Supervisor1872d0/1
Pending
Concierge Lead1882d0/1
Pending
Lead Security Officer1951d0/1
Pending

Hiring efficiency

2d median time-to-hire

Velocity

2dmedian

80% of roles close within 48 hours.

Fastest to fill

  • Lead Security Officer

    1 hire

    1d
    -1d
  • Guest Relations Lead

    1 hire

    2d
  • Operations Supervisor

    1 hire

    2d
  • Concierge Lead

    1 hire

    2d

Slowest to fill

  • Event Coordinator

    1 hire

    3d
    +1d
  • Guest Relations Lead

    1 hire

    2d
  • Operations Supervisor

    1 hire

    2d
  • Concierge Lead

    1 hire

    2d

AI hiring insights

3 findings
  • Security is hiring the strongest candidates

    1 hire at an average score of 95, against 90 across the workspace.

  • Event Coordinator takes longest to fill

    3 days on average against a median of 2. 1 hire on that record.

  • 5 hires have no client review

    Quality of hire is measured on the AI score alone until a review lands. Chasing these closes the loop on outcomes.

\ No newline at end of file diff --git a/scripts/__baseline__/analytics.render.html b/scripts/__baseline__/analytics.render.html new file mode 100644 index 0000000..6c2a288 --- /dev/null +++ b/scripts/__baseline__/analytics.render.html @@ -0,0 +1 @@ +

Analytics

Live System

How hiring is performing — rates, trends, comparison and what to act on.

Hiring performance

Across the workspace

Total hires

0

Avg time-to-hire

—

Quality of hire

—
avg AI score

Conversion rate

0%
0 of 0 applicants

Hiring funnel

Applied → Screened → Shortlisted → Interview → Hired

No applications on file yet. The funnel appears once the first candidate applies.

Hiring trend

Cumulative hires by month

Not enough history for a trend

No hires recorded yet. Hiring volume over time appears here once the first role closes.

Department performance

0 departments compared

Position performance

0 roles filled
Hires, quality, speed and review outcome by role
RoleHiresAvg scoreAvg daysReviewedRating

Hiring efficiency

Not enough dated hires

Velocity

—median

Velocity appears once hires carry an application date.

Fastest to fill

No role has enough dated hires to measure velocity yet.

Slowest to fill

No role has enough dated hires to measure velocity yet.

AI hiring insights

There is not enough hiring on file to draw a finding from yet. Insights appear as applications, hires and reviews accumulate.

\ No newline at end of file diff --git a/scripts/__baseline__/candidates.pre-migration.html b/scripts/__baseline__/candidates.pre-migration.html deleted file mode 100644 index f53d3ba..0000000 --- a/scripts/__baseline__/candidates.pre-migration.html +++ /dev/null @@ -1 +0,0 @@ -

Candidates

Live System

Review, compare and manage the candidate pipeline.

0 of 0 candidates

No candidates match your filters

\ No newline at end of file diff --git a/scripts/__baseline__/candidates.render.html b/scripts/__baseline__/candidates.render.html new file mode 100644 index 0000000..6f8a5dd --- /dev/null +++ b/scripts/__baseline__/candidates.render.html @@ -0,0 +1 @@ +

Candidates

Live System

Review, compare and manage the candidate pipeline.

0 of 0 candidates

Nobody is awaiting a decision

Candidates arrive here once their interview is completed, and leave once they are hired or declined.

\ No newline at end of file diff --git a/scripts/__baseline__/hired-history.pre-migration.html b/scripts/__baseline__/hired-history.pre-migration.html deleted file mode 100644 index be54cb4..0000000 --- a/scripts/__baseline__/hired-history.pre-migration.html +++ /dev/null @@ -1 +0,0 @@ -

Hired History

5 on recordLive System

Who was hired, for which position, and what has happened since.

Total hires

5

Most recent hire

Jul 22, 2026
Sophia Chen

Avg time-to-hire

2d
5 still active

Recent hiring timeline

Last 5 of 5
  1. Sophia Chen

    Guest Relations Lead

  2. Oliver Bennett

    Event Coordinator

  3. Aaliyah Patel

    Operations Supervisor

  4. Lucas Wright

    Concierge Lead

  5. Elena Rostova

    Lead Security Officer

Hired candidate records

5 people
Everyone hired, with position, client, score and time-to-hire
Status
SC

Sophia Chen

sophia.chen@email.com

Jul 22, 202690
Hired
OB

Oliver Bennett

oliver.b@email.com

Jul 20, 202691
Hired
AP

Aaliyah Patel

aaliyah.p@email.com

Jul 18, 202687
Hired
LW

Lucas Wright

lucas.w@email.com

Jul 15, 202688
Hired
ER

Elena Rostova

elena.r@email.com

Jul 12, 202695
Hired

Select a row to open the full record.

\ No newline at end of file diff --git a/scripts/__baseline__/hired-history.render.html b/scripts/__baseline__/hired-history.render.html new file mode 100644 index 0000000..a994e83 --- /dev/null +++ b/scripts/__baseline__/hired-history.render.html @@ -0,0 +1 @@ +

Hired History

0 on recordLive System

Who was hired, for which position, and what has happened since.

Total hires

0

Most recent hire

—

Avg time-to-hire

—
0 still active

Recent hiring timeline

Nothing yet

No hires yet

The timeline fills in as candidates are hired for a position.

Hired candidate records

0 people

No hires recorded yet

Hires appear here as positions close.

Select a row to open the full record.

\ No newline at end of file diff --git a/scripts/skill-check.mjs b/scripts/skill-check.mjs index 45c0fef..1890304 100644 --- a/scripts/skill-check.mjs +++ b/scripts/skill-check.mjs @@ -8829,22 +8829,69 @@ console.log('\n── Candidates vs Talent Pool ──'); const classes = (html) => (html.match(/class="[^"]*"/g) || []).join('\n'); const words = (html) => html.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); - const tally = (html) => (html.match(/<\/?[a-z][a-z0-9-]*/gi) || []) - .map((t) => t.toLowerCase()) - .reduce((acc, t) => ({ ...acc, [t]: (acc[t] || 0) + 1 }), {}); - const hiredNow = stripControls(await draw('/src/pages/admin/HiredHistory.jsx', 'hired-history', '/admin/hired')); - const hiredBefore = readFileSync(join(ROOT, 'scripts/__baseline__/hired-history.pre-migration.html'), 'utf8'); + const hiredBefore = readFileSync(join(ROOT, 'scripts/__baseline__/hired-history.render.html'), 'utf8'); record('Hired History paints the same styled elements, in the same order', classes(hiredNow) === classes(hiredBefore)); record('Hired History shows the same words', words(hiredNow) === words(hiredBefore)); - record('Hired History added only identity wrappers', (() => { - const b = tally(hiredBefore); - const n = tally(hiredNow); - const changed = [...new Set([...Object.keys(b), ...Object.keys(n)])].filter((t) => (b[t] || 0) !== (n[t] || 0)); - return { pass: changed.join(',') === '`s and changed + * nothing else. That was true, and it was only checkable while the page's + * DATA was frozen too: the comparison subtracts one whole render from + * another, so removing the invented hires moved every count and the + * arithmetic stopped describing the wrappers at all. Recapturing does not + * rescue it either — with the baseline equal to the render the tag delta is + * zero, and a predicate demanding a delta of two can never hold again. + * + * So the property is asserted directly instead of inferred from a + * subtraction. `UiTreeRenderer` gives a node its identity one of two ways: + * a type registered `wrap: true` is enclosed in `
`, and + * every other type takes the attributes on its own root element. The three + * checks below say exactly that, and they keep saying it as the page's + * content changes — which the tally could not. + */ + const hiredComposed = compMod.composePage('hired-history', ctx5).tree; + const hiredNodes = nodeMod5.walk(hiredComposed); + const hiredWrapped = hiredNodes.filter((n) => reg5.get(n.type)?.wrap === true); + /* The attribute list of every `
` that carries an identity. */ + const hiredWrappers = [...hiredNow.matchAll(/]*\bdata-ui-node="[^"]+"[^>]*)>/g)] + .map((m) => m[1]); + const hiredIds = [...hiredNow.matchAll(/data-ui-node="([^"]+)"/g)].map((m) => m[1]); + + record('Hired History wraps exactly the node types registered to wrap', + hiredWrappers.length === hiredWrapped.length, + `${hiredWrappers.length} wrapper div(s) for ${hiredWrapped.length} wrapping type(s): ` + + `${hiredWrapped.map((n) => n.id).join(', ') || 'none'}`); + + /* Unique, and every one a node the page actually composed. + NOT "one per composed node": an extension slot with no skills attached + renders nothing at all, which is correct and is why `hired-extensions-top` + has no identity here. Asserting a one-to-one mapping would be asserting + that every slot is always filled. What must hold is that nothing is + addressed twice and nothing is addressed that the page did not compose — + either would break the editor's and Owliver's ability to name a node. */ + const hiredComposedIds = new Set(hiredNodes.map((n) => n.id)); + const hiredDuplicated = hiredIds.filter((id, i) => hiredIds.indexOf(id) !== i); + const hiredStrays = hiredIds.filter((id) => !hiredComposedIds.has(id)); + const hiredSilent = hiredNodes.filter((n) => !hiredIds.includes(n.id)).map((n) => `${n.id} (${n.type})`); + + record('Hired History identities are unique and name composed nodes', + hiredDuplicated.length === 0 && hiredStrays.length === 0, + `${hiredIds.length} of ${hiredNodes.length} composed node(s) addressed` + + `${hiredSilent.length ? `; rendering nothing: ${hiredSilent.join(', ')}` : ''}` + + `${hiredDuplicated.length ? `; DUPLICATED: ${hiredDuplicated.join(', ')}` : ''}` + + `${hiredStrays.length ? `; STRAY: ${hiredStrays.join(', ')}` : ''}`); + + /* Identity, not presentation. A wrapper that picked up a class would be + styling the page from the tree, which is the one thing wrapping a node + must never do — it is why `nodeAttrs` emits `data-` attributes only. */ + record('Hired History identity wrappers carry no styling', + hiredWrappers.every((attrs) => !/\bclass=/.test(attrs)), + hiredWrappers.map((a) => a.trim()).join(' | ') || 'no wrappers'); record('Hired History carries node identity in the DOM', (() => { const ids = [...hiredNow.matchAll(/data-ui-node="([^"]+)"/g)].map((m) => m[1]); return ids.includes('chronology') && ids.includes('records'); @@ -9073,11 +9120,17 @@ console.log('\n── Candidates vs Talent Pool ──'); const words = (h) => h.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim(); const PAGES = [ - ['candidates', '/src/pages/admin/Candidates.jsx', '/admin/candidates', 'candidates'], - ['talent-pool', '/src/pages/admin/TalentPool.jsx', '/admin/talent-pool', 'talent-pool'], - ['analytics', '/src/pages/admin/Analytics.jsx', '/admin/analytics', 'analytics'], - ['candidates-analysis', '/src/pages/admin/CandidatesAnalysis.jsx', '/admin/candidates-analysis', 'candidates-analysis'], - ['control-center', '/src/pages/admin/ControlCenter.jsx', '/admin', 'control-center'], + /* The fourth column is the baseline FILE, not a stem. Three pages were + recaptured after the final-selection work changed what they render and + are named `.render.html`; the other two still hold genuine + pre-migration markup and keep the name that says so. Deriving the + filename from the page would force both sets to share one suffix, and + whichever suffix won would be a lie about half of them. */ + ['candidates', '/src/pages/admin/Candidates.jsx', '/admin/candidates', 'candidates.render.html'], + ['talent-pool', '/src/pages/admin/TalentPool.jsx', '/admin/talent-pool', 'talent-pool.pre-migration.html'], + ['analytics', '/src/pages/admin/Analytics.jsx', '/admin/analytics', 'analytics.render.html'], + ['candidates-analysis', '/src/pages/admin/CandidatesAnalysis.jsx', '/admin/candidates-analysis', 'candidates-analysis.pre-migration.html'], + ['control-center', '/src/pages/admin/ControlCenter.jsx', '/admin', 'control-center.pre-migration.html'], ]; for (const [page, modulePath, route, baseline] of PAGES) { @@ -9089,7 +9142,7 @@ console.log('\n── Candidates vs Talent Pool ──'); React.createElement(UiEditingProvider, { page }, React.createElement(Page)))) )); - const before = readFileSync(join(ROOT, `scripts/__baseline__/${baseline}.pre-migration.html`), 'utf8'); + const before = readFileSync(join(ROOT, `scripts/__baseline__/${baseline}`), 'utf8'); /** * A migration may INSERT a wrapper class — grouping three siblings under