Skip to main content

Fable full-portfolio review: XIS M365 / Azure Assessment Tool

Repository: X-Centric-IT-Solutions/prj-xis-m365_azure_assesor Reviewed main: c728ea48e71674e8e4b052b7b16d5933a32a080f (pristine detached checkout under evidence/) Review date: 2026-09-28 Reviewer: Fable, fresh context, read-only. Every code claim below is bound to the main SHA above unless a candidate SHA is named. Every candidate claim is bound to the SHA named beside it. "Observed" means I read it in the evidence; "inferred" is marked as such.


1. Executive summary​

State of the project. The committed code on main is substantially better than the board suggests. The security core is real: per-customer SQLCipher databases under envelope-wrapped keys, a fail-closed safeStorage secret store, a typed IPC contract with per-handler validation and ownership assertions, encrypted-only .xisnap export, fail-closed report-file ownership for retention, and a SQLCipher-v4 adapter that verifies its cipher profile and key before handing out a connection. The CIS 287-control baseline is on main (PR #261), derived from the licensed workbooks by a hash-pinned extractor and drift-tested. The WAF denominator is pinned (59 items, manifest 2026.09.2). The build, typecheck, test, lint and audit jobs are green at the reviewed SHA. The one red CI job is the CIS independent human-approval gate, and that framing holds: it fails because no approval evidence and no licensed workbook secret are supplied, not because of a code defect.

What blocks v1. Five things, in order of leverage:

  1. Nothing has been run as a packaged app, and no collection has run against a live tenant. Every document says so. The native packaging matrix on main runs only on manual dispatch and has not been dispatched since it was rewritten. Until an installer exists, #109, #110 and #237 cannot start.
  2. The retention commitment is not reachable from the product. The 14-day purge exists in the main process and is tested, but no renderer code calls projects:close-engagement, projects:record-sign-off or projects:reopen-engagement. A consultant cannot close an engagement, so closed_at is never set and the sweep never has a candidate. DATA-HANDLING.md §6.2 tells customers the tool enforces this. It does not, today. This is #118, open and unstarted.
  3. Two unquarantined Azure rules and two WAF rules still violate the project's own "collection failure is never a verdict" rule. The Key Vault evaluators pass vacuously when the vault listing failed or no subscription was collected. Two WAF evaluators report a permissions failure as fail. These are exactly the false-pass and false-negative classes the repo has documented three times.
  4. Deliverables carry no provenance (#166). The shipped pdfkit report prints project, customer, snapshot label and date; it does not print benchmark versions, the rule-set hash, or the app version. The new HTML report template (#207) has landed but nothing renders it; export:pdf still uses pdfkit.
  5. The CIS approval gate keeps main red by design, which means a real regression on main would look identical to the expected state. Either supply the approval evidence or move the gate off the push path.

What you can start today. Dispatch the native package matrix on main (it targets GitHub-hosted Windows and macOS runners; the "no Windows host in the fleet" blocker on the board does not apply to it). Rule on the two owner-decision cards using the dispositions in §8; four of the seven candidates they hold are already on main by content and should be retired as shipped. Open two bounded issues: the fail-closed evaluator fix and the #118 renderer wiring. Fix worktree provisioning before dispatching any more automated writers; a third of the fleet's worktrees are checked out with modes that manufacture false test failures.

Board and branches. Of 323 blocked cards, roughly 10 are genuine blockers. The rest are GitHub-issue mirrors, multi-round plan/review loop residue, and a fleet-tooling program. Of 83 branches ahead of main, at least 18 are exact heads of merged PRs and about 40 more are superseded intermediates; roughly 10 hold work worth keeping. The canonical working checkout is not lost work: its dirty tree is, by every measurable signal, main's content overlaid on a stale branch.


2. Verified facts and what could not be verified​

2.1 Verified (observed in evidence)​

FactValueSource
main SHAc728ea48e71674e8e4b052b7b16d5933a32a080finputs/manifest.json, evidence/main-log-40.txt
main commit count217manifest
Last mergePR #391, merged 2026-09-28T03:24:02Zevidence/prs-closed-all.json
Open / closed issues186 / 81manifest, issues-compact.txt
Open / closed PRs0 / 124manifest
Kanban323 blocked, 37 todo, 1 ready, 1 triage, 481 done, 54 archivedmanifest, kanban-active-compact.json; 481 done confirmed by grep
Branches ahead of main83wip-branch-census.json
Worktrees316worktree-list.txt
CI at main10 jobs: 6 success, 1 failure (CIS independent approval gate), 3 skipped (native-package-matrix, native-package, native-package-evidence: dispatch-only)ci-main-jobs.json
Failing stepCIS independent approval gate (blocks until human approval), exit 1: two licensed workbooks absent, CIS_HUMAN_APPROVALS absent, three pending subjects (cis-azure-2.2.1, cis-azure-2.2.2, sourceDerived)ci-main-failure-log.txt lines 419-426
Same job's other stepscommitted-bytes authority check ok, adversarial suite 47 tests OKfailure log lines 290-380
Main CI conclusion historyevery push run on main in the last 40 runs is failure (2026-09-22 through 09-28)actions-runs-40.json
Last successful installer buildWindows Build (unsigned installer) dispatch at 89f23a22, 2026-09-23; and at 106ad99c, 2026-09-21. Both predate the current thin-front-door workflowactions-runs-40.json
Live processes at launchlane controller (controller.py run), one planner one-shot (hermes chat --profile coordinator, 43 s old), one coordinator shell doing git fetch in the m365-assessor-issue-171 worktree, Hermes infrastructure. Zero implementation or review workerslive-process-census.txt
Rule inventory173 cis-m365 + 136 cis-azure + 12 waf = 321 rulesgrep on resources/rules/*.json
Rule-file versionscis-m365 7.0.0, cis-azure 6.0.0, waf 2024rule file headers
Coverage matrix totalstoolAutomated 2, manualWorkflow 278, toolingGap 7 (of 287)resources/cis-coverage-matrix.json lines 5-29
Quarantined rules13: 11 cis-azure + waf-se-04 + waf-re-01cis-quarantine.test.ts lines 32-46, rule JSON
Non-manual fn evaluators referenced by rules35 (14 m365, 13 azure, 8 waf) + 3 JMESPath (cis-azure Defender pricing, all quarantined)grep counts
Vitest cases1794 it(/test( across 91 filesgrep count
Driverbetter-sqlite3-multiple-ciphers 13.0.3, prebuilds pinned by SHA-256 for win32-x64, darwin-x64, darwin-arm64, linux-x64package.json, scripts/native-package-policy.json
@journeyapps/sqlcipherabsent from root package.json; present only in scripts/sqlcipher-legacy-harness/package.json (nested, its own lockfile, own CI job)grep

2.2 Could not be verified (read-only, no execution)​

  • Whether npm test passes on a fresh clone without Python and openpyxl. CI installs openpyxl before npm test because src/main/rules/cis-authority.test.ts spawns the Python adversarial suite. docs/BUILD-AND-TEST.md lists only Node as a prerequisite.
  • Whether the native package inspector produces pass for any of the 11 requiredCheckIds on real targets. The workflow has not been dispatched since the WP6 seam landed. The inspector header (scripts/inspect-native-package.mjs lines 8-15) says capability-dependent checks report blocked until WP3/WP4/WP5 adapters exist; WP3 and WP4 have since merged, WP5 has not.
  • sandbox: true behaviour on macOS and Windows. Never observed, per docs/SIGNING.md line 48 and docs/BUILD-AND-TEST.md line 125.
  • The canonical checkout at /root/prj-xis-m365_azure_assesor: file reads were denied. The verdict in §7.3 is inferred from the coordinator's status and diffstat only.
  • The lane controller source (controller.py line 583) cited by the operations card: read denied. The card's claim stands unverified.
  • Content equality between candidate SHAs and main for the retire-as-shipped recommendations in §6. I verified by reading main for the named features, not by diffing the candidates.

3. Committed-code review findings (main @ c728ea48)​

Severity per the owner's scale. "Class" separates code defect from missing evidence from deliberate gate.

Critical​

F1. Retention lifecycle has no product path. Class: code gap that makes a documented customer commitment false. src/preload/index.ts:38-40 exposes projects:close-engagement, projects:reopen-engagement, projects:record-sign-off; src/main/ipc.ts:894-957 implements them and runs the sweep on close. No file under src/renderer/src calls any of them (grep returned only a test fixture in ProjectList.test.tsx:37-38). retention-service.ts:39-55 purges only rows where closed_at is set (db-service.ts:1589-1597). Nothing sets it. docs/DATA-HANDLING.md §6.2 ("The application now enforces the part of the commitment that lives on the consultant's machine") and SECURITY.md §7 describe enforcement that a consultant cannot trigger. Remedy: implement #118 as a bounded renderer change: an engagement panel with Close, Reopen, Record sign-off, and a countdown derived from projects:rollups (closedAt is already returned, ipc-channels.ts:112). Add a DOM test that clicking Close invokes the channel. Until it lands, amend DATA-HANDLING §6.2 to say the lifecycle actions are main-process only and not yet exposed.

F2. Key Vault rules pass vacuously on collection failure or empty collection. Class: code defect (false pass). src/main/rules/evaluator-registry.ts:930-978 (checkKeyVaultPublicAccess, checkKeyVaultSoftDelete) iterate sub.keyVaults and return pass when the loop finds nothing. drainIter (azure-collector.ts:557-561) returns [] on failure and records the error only in snapshot.errors; these two evaluators never consult it, and they also pass when snapshot.azure is empty. Rules cis-azure-4.1.1 and 4.1.2 (resources/rules/cis-azure.json:217-243) are not in the quarantine set and carry sectionRef: "—" (unmapped legacy), so they enter the cis-azure compliance denominator with a verdict that can be "all vaults compliant" for a tenant whose Key Vault listing was denied. The storage and NSG checks in the same file (lines 737-796, 1438-1537) were hardened for exactly this; these two were missed. Remedy: apply the same three-way pattern (unavailable, malformed, failing) as checkStorageSecureTransfer; return error when snapshot.azure is empty or any subscription's keyVaults collection is missing or errored. Add the two rules to azure-evaluator-correctness.test.ts with a denied-collection fixture. Alternatively quarantine them pending that fix; either closes the false pass.

F3. No packaged build has been produced from main, and no smoke test or live-tenant run has ever been observed. Class: missing evidence, not a code defect. plans/00-context.md:88-89, docs/BUILD-AND-TEST.md:140-141, docs/DATA-HANDLING.md:468-470 all state it. actions-runs-40.json shows the last installer dispatch at 89f23a22 (2026-09-23) under the retired standalone workflow. The current ci.yml native jobs (lines 408-677) are workflow_dispatch/workflow_call only and have no recorded run. Remedy: dispatch ci.yml on main once; read the four verdict.json artifacts; hand the NSIS and DMG artifacts to the human owner of #109/#237. This is the single highest-leverage evidence action available.

Required​

F4. WAF evaluators report collection failure as non-compliance. Class: code defect (false negative). evaluator-registry.ts:1583-1601 (checkNetworkSegmentation, rule waf-se-05) and 1680-1698 (checkMonitoringConfigured, rule waf-oe-02) treat a null or empty collection as fail without consulting snapshot.errors. safeFetch returns null on failure (azure-collector.ts:585-589); drainIter returns []. A missing Reader assignment on one subscription is therefore reported as "no NSGs" or "no diagnostic settings". PRs #389 and #390 fixed only the zero-subscription vacuous-pass case. docs/DATA-HANDLING.md §4 promises the opposite. Remedy: check collectionFailed(snapshot, 'azure/<sid>/networkSecurityGroups') and 'azure/<sid>/diagnosticSettings' per subscription and return not-assessed (consistent with the sibling checkActivityLogAlert at line 1348) when any subscription is uncollected.

F5. Rules-engine initialization failure does not stop the app. Class: code defect (fail-open). src/main/index.ts:170-175 logs and continues when rulesEngine.initialize() throws; rules-engine.ts:403-406 only warns when the bundled directory is missing. A packaged build with an empty resources/rules (the documented stale-release/ trap, README.md:349-360) loads zero rules, and assessment:run then persists zero findings and the PDF prints "0 controls were checked". Settings shows loadErrorCount, but nothing gates assessment. This is #168, labelled mvp-critical and unstarted. Remedy: in ipc.ts:1445 reject assessment:run when rulesEngine.getStatus().ruleCount === 0 or loadErrors.length > 0, with a message naming the rules directory; in index.ts treat a missing bundled directory on a packaged build as fatal (showErrorBox and quit, as the secret-store gate does at lines 133-151).

F6. Deliverables carry no benchmark or rule-set provenance. Class: code gap (#166, mvp-critical). export-service.ts:343-353 prints project, customer, snapshot label, generation date. The engine exposes rulesHash and rulesVersion (rules-engine.ts:346-355) and the rule files carry version and benchmarkVersion; none reach the PDF, Excel or PPTX. Only the JSON export records app.getVersion() (line 1548). A customer cannot tell which benchmark revision or rule set produced a report. Remedy: add one provenance block to every exporter: benchmark ids with version, rulesVersion, rulesHash, app version, snapshot id. Source it from one helper so the four formats cannot disagree, and drift-test it the way coverage counts are drift-tested.

F7. The report template that meets the report standard is landed but unwired. Class: delivered code without delivered outcome. src/main/report/template.ts and report-data.ts exist with 78 + 28 + 12 + 6 tests; grep for renderReportHtml, toReportData(, printToPDF finds no caller outside src/main/report/. ipc.ts:1560-1620 still calls pdfkit. #208 (printToPDF renderer) has no candidate. The customer-facing report is therefore the older pdfkit layout that #211 planned to retire. Remedy: owner decision (§8): ship v1 on pdfkit plus F6 provenance, or make #208 a v1 item. If the latter, report-data.ts:344-364 must be corrected first (F8).

F8. The report data layer names permissions the tool does not request. Class: code defect in customer-facing text. report-data.ts:344-364 maps directoryRoles to RoleManagement.Read.Directory and sensitivityLabels to InformationProtectionPolicy.Read.All. The collector and README require Directory.Read.All and SensitivityLabels.Read.All (README.md:37-44; src/renderer/src/lib/permissions-table.ts:17,43). A "resolves with" line in a report would tell the customer's admin to grant a permission the tool does not use. Not reachable by consultants until F7 is wired, so Required rather than Critical. Remedy: derive RESOURCE_PERMISSIONS from permissions-table.ts (one source) or add a drift test that every value appears in the README table.

F9. Backlog target validation permits plaintext token transport and path-shaped identifiers. Class: security hardening gap. ipc.ts:528-531 accepts any ^https?:// organization URL; azure-devops-adapter.ts:55,100 then sends Basic auth with the PAT to it. ipc.ts:541-546 accepts any non-empty owner/repo; github-adapter.ts:42,70 interpolates them into the API path, so an owner containing / or .. changes the endpoint the PAT is sent to. Both are consultant-supplied values on the consultant's own machine, so impact is bounded, but the PAT is the one write credential in the product. Remedy: require https: for orgUrl and a dev.azure.com or visualstudio.com host; restrict owner/repo to ^[A-Za-z0-9_.-]+$.

F10. docs/DATA-HANDLING.md disagrees with the collector on what is read. Class: documentation defect in a customer-facing document. Lines 65-69 say fifteen Azure resource families; azure-collector.ts:711-729,1015 collects sixteen, the sixteenth being activityLogAlerts. Lines 95-97 say activity-log alert rules are "alert rules this tool does not collect"; azure-collector.ts:1014-1020 collects them. §6.2 overstates retention enforcement (F1). Remedy: reconcile the two sections; add a drift test that the documented resource list equals the collector's AzureSubscriptionData keys (the M365 table already has this shape).

F11. Build prerequisites are understated. Class: documentation defect. npm test on main executes cis-authority.test.ts, which spawns python3 -B scripts/test-cis-authority.py requiring openpyxl; ci.yml:28-38 installs both before testing. docs/BUILD-AND-TEST.md:14-19 and README.md:302-309 list Node only. A consultant following the runbook on a clean laptop gets a red suite. Remedy: either list Python 3.12 + openpyxl 3.1.5 as a test prerequisite, or make that one test skip with an explicit "environment not provisioned" status when Python is absent, never silently pass.

Optional​

F12. Cancellation is not honoured on the activity-log-alert stage. azure-collector.ts:1015-1020 calls drainIter without signal, unlike every sibling call. A cancel during that page fetch waits it out. Remedy: pass signal.

F13. Secret-store writes are not serialized. key-vault-service.ts:148-193: writeEntry and deleteSecret each load the whole JSON, mutate, and atomically replace. Two concurrent handlers (for example backlog:configure racing projects:create) can lose one write. getOrCreateExportKey (snapshot-portability-service.ts:136-142) has a check-then-write race. Remedy: a single promise-chain mutex around load/save, same pattern as withLifecycleLock.

F14. Global Administrator count of zero resolves to na. evaluator-registry.ts:246-250. No real tenant has zero Global Administrators; zero means the role page shape was not what the evaluator expected. plans/00-context.md:121-124 records this as a deliberate Terra ruling, so it is a decision to revisit, not a bug to fix silently. Remedy: return error with evidence naming the role enumeration when count is zero, or keep na and say why in the finding text.

F15. Large-file and cohesion smell. src/main/ipc.ts 2111 lines, db-service.ts about 3900 lines, evaluator-registry.ts 1766 lines, export-service.ts over 700 lines with four renderers in one module. Remedy: register handlers per domain module (ipc/projects.ts, ipc/exports.ts, …) and split DbService into registry, customer-data, and artifact-reservation modules behind the same public class. No behaviour change; do it when the security-core work above is done, not before.

F16. Repository hygiene. Main carries about 150 per-issue receipt logs and reports at the root (issue233-harness-policy-receipts/*, issue233-pptx-receipts/*, issue2xx-*-report.md, cis-287-remediation-r3-report-*.md, waf-slice1-report-*.md), two vendored tarballs, a proof-of-concept tree (scripts/sqlcipher-poc) and a nested legacy harness with its own lockfile that CI installs on every push (ci.yml:333-371, about 100 s). Remedy: move receipts to docs/evidence/ or an evidence branch; keep the legacy-harness job but run it on a path filter or dispatch.

F17. Minor documentation drift. SECURITY.md:295-296 says the app never calls shell.openExternal; ipc.ts:685 does, to a fixed allowlisted URL (a private-repository page consultants may not be able to open). plans/00-context.md:93-105 still says 294/283/11 and 27 verdict-producing rules; README says 305/283/22/16 and the drift test agrees with README. SECURITY.md:216-220 says reason is optional for manual-verdict; curation-service.ts:263-265 requires it. Remedy: one docs pass; add the sectionRef "—" and Option R rules to SECURITY §4.

No remediation needed (verified sound)​

  • IPC boundary: UUID, length, enum and shape validation on every handler; assertSnapshotBelongsToProject on every project+snapshot pair (ipc.ts:107-120 and call sites); import channels reject a smuggled filePath before any dialog (ipc.ts:1999,2037); evidence attachments must be Uint8Array (ipc.ts:611-617).
  • Electron hardening: contextIsolation, nodeIntegration: false, sandbox: true, window-open deny, will-navigate origin/path check, webview deny, header-only environment-aware CSP (index.ts:29-116); fuses set by electron-builder (package.json:128-135).
  • Database: envelope key wrap with AES-256-GCM (db-service.ts:382-401); adapter pins and reads back the SQLCipher v4 profile before and after keying, rejects plaintext, short and absent files, and never creates on an open path (sqlcipher-adapter.ts:339-557); per-connection pragmas read back on every open (db-service.ts:1227-1243); migration steps atomic (1310-1325).
  • Retention purge: strip, re-key to a fresh file, swap the registry key before rename, crash recovery of the sibling, purged_at written last (db-service.ts:1775-2069); report-file removal only through the rename-quarantine ownership boundary keyed on dev:ino plus content digest (db-service.ts:626-712, 1641-1753).
  • Export reservation: exclusive wx+ claim, all bytes written through the retained descriptor, identity re-checked before registration (db-service.ts:3391-3676).
  • Snapshot portability: unconditional AES-256-GCM with scrypt, KDF parameter bounds on import, digest before HMAC before decrypt, denylist scan on plaintext (snapshot-portability-service.ts).
  • Collectors: Graph @odata.nextLink origin and path pinned (m365-collector.ts:49-72), page cap, per-element validation for role members; ARM calls bounded by timeout, retry with Retry-After, concurrency cap, abortable at every stage except F12.
  • Compliance math: one shared effectiveStatus and coverage aggregation (src/shared/coverage.ts) used by dashboard, registry rollup and all exporters; manual verdicts count only when evidence-qualified.
  • Counts that reach customers are derived: coverage-counts.test.ts drives the real engine and parses README; benchmark-denominators.test.ts derives 160/127 from committed manifests; the WAF manifest is byte-for-byte reproduced by its generator.

4. Priority-area deep dives​

4.1 CIS baseline delivery​

Observed on main: resources/rules/cis-m365.json (173 rules, version 7.0.0, "all 160 recommendations, derived from the licensed workbook … by scripts/generate-cis-rules.mjs") and cis-azure.json (136 rules, version 6.0.0, all 127). The 22 extra rules are the pre-v7/v6 legacy set declared unmapped in resources/control-indexes/ (13 M365 + 9 Azure per docs/CIS-SOURCE-AUTHORITY.md:213-214). The coverage matrix records 2 tool-automated controls (cis-m365-5.2.1, cis-m365-6.1.1), 278 manual workflows, 7 tooling gaps. README states exactly this and the drift test enforces it.

The trust chain (docs/CIS-SOURCE-AUTHORITY.md:56-118) is sound in design: an externally supplied source-binding receipt with an externally recorded SHA-256, a hash-pinned extractor, committed corpus bytes, generated rules, and a coverage matrix that the generator reproduces byte-for-byte. Two things remain by construction, not by defect:

  • The approval gate: verify-cis-authority.py --require-approval needs CIS_HUMAN_APPROVALS_JSON and _SHA256 naming three subjects. Owner action #376.
  • The licensed re-extraction: hosted CI cannot hold the workbooks; --ci verifies committed bytes instead and says so. Owner action #377 on an authorized machine, or provision CIS_LICENSED_WORKBOOKS_B64.

Open sub-issues with preserved candidates: #373 (feat/issue-373-142b-orphan-rules @ 1a317e0, 1 ahead / 54 behind), #374 (feat/issue-374-142c-severity-provenance @ 92d4e6c, 1 ahead / 54 behind), #253 parity (feat/issue-253-parity-r2 @ 97d920f). #375 is a decision on the two pending tuples in resources/cis-authority/pending-unmapped.json. #378 closes #142 when the others are done.

One design caution: the CIS gate is a required push-path job. While approval is genuinely absent, every push to main is red, and actions-runs-40.json shows that has been true for at least six days. A green typecheck/test/lint run is still visible per job, but the run-level signal is lost. Recommendation in §9.4.

4.2 WAF​

Observed on main: resources/waf-baseline/waf-baseline-manifest-2026.09.json (59 items, 0 fully automated, 4 partial, 55 consultant-review), retained page bytes under sources/pages-2026-09-15/, a strict validator shared by generator and runtime import, 135 drift tests. docs/waf-baseline-reconciliation.md is honest: eleven of twelve bundled rules cite a framework code they do not address, and the correspondence file corrects the mapping without editing the rule files.

What is not done: the rule files still carry the wrong sectionRef (customer-visible), two WAF evaluators still have the false-negative class (F4), and the Security pillar port (#230) is an approved candidate awaiting merge authority: assessor/issue230-report-0498d3a1-r1 @ c4fcb028, 1 ahead / 8 behind main. Its base 0498d3a1 is the parent of main's last merge, so the rebase risk is limited to PR #391's WP3-B2 files (scripts/native-package-workflow-drift.mjs and its tests), which the candidate does not touch ("scripts/ untouched" per the owner card).

4.3 Database and encrypted DB​

The cutover is complete on main: src/main/sqlcipher-adapter.ts is the only driver path; db-service.ts creates and opens through it; the corpus fixtures (test/fixtures/sqlcipher-corpus/*) were frozen from the genuine SQLCipher amalgamation and are read back through the new driver. #229, #231, #232, #233 are closed. #235 proof stack (WP1-WP4) merged via PRs #379, #380, #385. #234 is labelled ready; its substance (drop the JourneyApps devDependency so a Windows npm ci never compiles it) is on main: root package.json has no @journeyapps/sqlcipher, PR #388 asserts no override survives, and the only remaining reference is the nested legacy harness with its own lockfile. What remains for #234 is to compare its acceptance criteria against main and close.

Residual risks: (a) the IncompleteDatabaseFileError guard rejects files under one page, which is correct, but a database whose -wal sidecar holds the only committed rows is accepted and left to SQLite recovery; the adapter documents this. (b) deleteProject (db-service.ts:1484-1499) removes -journal and -wal but not -shm or a stale .purge sibling; both are ciphertext under a key that no longer exists after the registry row is gone, so cryptographic erasure holds. Nit only.

4.4 Retention and erasure​

The main-process implementation is the strongest part of the codebase (see "No remediation needed"). The gap is F1: there is no user interface for close, reopen, sign-off, or countdown, so the purge can never be reached. #160 ("export artifacts can escape the purge, and purge can delete a file it did not write") is, by content, resolved on main: report rows record file_identity, removal goes through the ownership boundary, and a missing customer database fails the purge closed rather than unlinking (retention-service.ts:93-131). The open issue should be verified against its acceptance criteria and closed; the preserved branch opencode/issue-160 @ 3620ca6 is superseded by the PR #192 lineage that landed.

4.5 Reports and export integrity​

Consistent compliance math across PDF, Excel, PPTX, JSON, briefing and dashboard is real (shared coverage.ts). Suppressed findings move to a dedicated section rather than disappearing; manual verdicts are labelled "(manual)" and attributed. The evidence appendix is metadata-only in PDF and Excel; attachment bytes never leave the encrypted database. The .xisnap path is encrypted-only, and the renderer drops the passphrase from state after export (ReportsPanel.tsx:245).

Gaps: F6 (no provenance), F7 (report standard unwired), F8 (wrong permission names in the new data layer). The old pdfkit report uses WinAnsi fonts, so non-Latin customer names will render as boxes; the new template self-hosts IBM Plex and Space Grotesk and has an escaping contract enforced by a grep test. That is a real reason to prefer #208 for v1 if the owner can afford it.

4.6 Packaging and CI​

ci.yml on main has eight jobs on push plus three dispatch-only native jobs. Strengths: audit gate with no exception list (scripts/audit-check.mjs), dependency policy that fails on symlinked or unsupported packages (scripts/dependency-policy-core.mjs), authored-candidate checkout for the CIS jobs, native matrix derived from one policy file with per-target prebuild SHA-256 (scripts/native-package-policy.json), builder artifact manifest as the only way to locate the distributable, an aggregate gate that cannot convert blocked to pass.

Gaps: (a) the native path has never run since the seam landed (F3); (b) the inspector's supplyChain.sbomPath, licenceInventoryPath, provenancePath are requested (ci.yml:532-536) but nothing on main generates an SBOM, so supply-chain will report blocked (#169, WP5); (c) push CI is red by design (§4.1); (d) the native-package-evidence job hard-codes expectedJobId 1101-1104 (ci.yml:658-661) while matrix.jobId comes from the policy generator; a mismatch will fail the aggregate. I could not confirm the generator emits those values. (e) windows-build.yml is a thin workflow_call front door; the run history under its old name is the only installer evidence and predates the current code.


5. Complete issue inventory​

Classification values: merged-complete, active-with-evidence, preserved-candidate, stalled, technically-blocked, owner-decision, owner-action, human-gate-later, superseded/duplicate, deferred-by-decision, evidence-incomplete.

5.1 Epics and MVP-critical items​

#Title (short)ClassificationCandidate or blockerNext action
3Epic: verify and ship a packaged buildtechnically-blockedneeds F3 dispatch; children #109 #110 #111 #236 #237 #238dispatch native matrix on main
4Epic: close the benchmark coverage gapactive-with-evidencePR #261 on main; children #373-#378run the CIS closure chain (§9.2 step 8)
115Epic: engagement lifecycle and retentionactive-with-evidence#116 #117 merged; #118 unstarted (F1)open bounded renderer issue
118Retention 3/3: close, sign-off, countdown UIstalled (not started)worktree claude/issue-118 at 71f6106, 0 commits aheadimplement; IPC already exists
142Author the 287-control baselineactive-with-evidencemerged via PR #261 (106ad99c); residual in #373-#378keep open until #378
147Import a CIS release from workbook as reviewable diffmerged-complete by contentscripts/extract-cis-corpus.py, generate-cis-rules.mjs --check on main; opencode/issue-147 @ 6926adc supersededverify acceptance criteria, close
160Export artifacts escape purge / purge deletes foreign filemerged-complete by contentfile_identity + ownership boundary on main (PR #192 lineage); opencode/issue-160 @ 3620ca6 supersededverify, close
162Real SDK shapes make 13 rules return false resultsactive-with-evidence13 rules quarantined on main; evaluators corrected; azure-evaluator-correctness.test.ts 46 cases; claude/issue-162 @ 6af144e supersededun-quarantine only through #375/#376 approval
164Evaluate every subscription; NSG and alert false-passtechnically-blocked on #162activity-log alerts now collected and evaluated per subscription; quarantinedclose as duplicate of #162 once un-quarantined, or keep as the "prove on real data" gate
166Provenance in every deliverablestalled (not started)worktree opencode/issue-166 at b568c1d, 0 aheadimplement F6
168Fail closed on permissions, unused IPC, unavailable rulesstalled (not started)noneimplement F5
207Reports 2/5: paged HTML templateactive-with-evidencetemplate on main; PR #386 merged; claude/issue-207 @ c1294bc supersededclose #207 when #208 decision made
208Reports 3/5: printToPDF renderertechnically-blockedno candidateowner decision (§8 D-7)
227Epic: X-Centric PDF report standardactive-with-evidence#206 done, #207 landed, #208-#210 opendecide v1 scope
229Replace Windows-unsupported SQLCipher bindingactive-with-evidence (parent)#231-#233 closed; adapter on mainclose when #234-#238 resolved, or close now and let children stand
230Complete versioned WAF baselinepreserved-candidate + owner-decisionassessor/issue230-report-0498d3a1-r1 @ c4fcb028, APPROVED, 1 ahead / 8 behindgrant merge authority; rebase; merge
234Encrypted DB 4/8: convert fixtures, cross-driverpreserved-candidate, mostly landeddevdep drop on main (PR #388, root manifest clean); feat/issue-234-devdep-drop @ 683ed61, feat/issue-234-r5 @ 4ea90ca, feat/issue234-boundary-r2 @ b657963compare acceptance criteria to main; close
235Encrypted DB 5/8: purge, rotation, crash, residue proofsactive-with-evidenceWP1-WP4 merged (#379, #380, #385); feat/issue-235-wp4 @ 0310e36e likely superseded by PR #380 head d6f1cb35verify by diff; close
236Encrypted DB 6/8: native packaging matrix and inspectionactive-with-evidenceWP3-A (#383), WP3-B1 (#384), WP3-B2 (#387, #391), WP4 (#385), WP6 seam on main; WP1 42204a1 and WP2 a33b2c2 unmerged; WP5 absentdispatch matrix; integrate WP1/WP2 if their checks report blocked
3, 109HUMAN: smoke test packaged apphuman-gate-laterneeds installerafter dispatch
110HUMAN: live tenant end to endhuman-gate-laterneeds installer or dev build plus tenantafter #109
111HUMAN: code signingowner-action / deferredREADME discloses unsigned internal buildsowner confirms unsigned v1
237HUMAN: NSIS + encrypted lifecycle on Windowshuman-gate-laterneeds NSIS artifact from dispatchafter dispatch
238Encrypted DB 8/8: adoption and rollback gateowner-decisionneeds #237 evidenceafter #237

5.2 Other open product issues​

#Title (short)ClassificationCandidate or blockerNext action
5Epic: collection architecture (Resource Graph)deferred-by-decisionpost-mvpnone
6Epic: consulting workflowdeferred-by-decisionpost-mvpnone
23Migrate to Resource Graphdeferred-by-decisionnone
59Passphrase / 2FA unlockdeferred-by-decisionunlock_mode column reservednone
88Epic: UI redesigndeferred-by-decisionnone
94Command palettedeferred-by-decisionopencode/issue-94-palette-core @ 5d7c783 preservedarchive branch
131Revisit TCM at GAdeferred-by-decisionnone
134Research four Graph sourcesowner-decision (needs-decision)claude/issue-134 @ 189fa0e (docs)owner reads the research, decides
137Map 63 Entra controlspreserved-candidate, stalledopencode/issue-137 @ 38b03c0, 7 ahead / 77 behind; Sol verification card tododecide: port docs or supersede by #142 source-derived rules
139Map 46 non-TCM M365 controlspreserved-candidate, stalledopencode/issue-139 @ e129186, 3 ahead / 92 behindsame as #137
140Map 127 CIS Azure controls to ARM providersmerged-completePR #244 (87baf60) landed plans/specs/azure-provider-mapping.md and TSV; opencode/issue-140 @ c2ab0fe (43 ahead) supersededclose; delete branch and ~45 review worktrees
146Derive permission ask from rulestechnically-blocked on #161rules carry permissions; UI table hand-maintainedafter #161
161Wizard asks five permissions; the ask is twelveowner-decision (needs-decision)code and README consistently say five collector-exercised permissionsowner rules: v1 ask = collector set (current) or rule-declared set
169Least-privilege CI, SBOM, secret scan, smoke gatespreserved-candidate, stalledfeat/issue169-overflow-r1 @ 66beb61, 6 ahead / 60 behind; CI seam partially covers permissionsre-scope to SBOM + dependency review + secret scan on top of WP6
170, 214, 215, 216Permission preflight 1/3-3/3technically-blockedafter #146none until #161
173Accessibility baselinepreserved-candidate, stalledopencode/issue-173 @ bf63d5d, 2 ahead / 86 behindport or drop; not v1
174Snapshot comparedeferred-by-decisionnone
175Select findings before backlog exportdeferred-by-decisionclaude/issue-175 @ fef9d75 preserved; findingIds already accepted by IPCarchive
179Keyboard-shortcut sheetdeferred-by-decisionfeat/issue179-backlog-currentmain @ e274d72archive
180, 181, 184UI redesign leavesdeferred-by-decisionnone
183Enable Cancel run once #171 landstechnically-blocked label stale#171 closed; collection:cancel on mainre-triage: verify CollectionRun has the button, then close or make ready
188Claim visibility reconcilerdeferred-by-decisionclaude/issue-188 @ 9d8158aarchive
199Replace unsupported npm chainssuperseded/duplicatePR #246 landed dependency policy; PRs #200/#243 closed unmerged; claude/issue-199-* and claude/pr200-* supersededclose
203Scalable per-resource evidenceactive-with-evidenceresource_facts, pagination core on main; IPC wiring candidate assessor/issue223-ipc-20260923T0045Z @ 07f175bpost-v1 unless #225 pilot is wanted
209, 210Reports 4/5, 5/5technically-blocked on #208after D-7
212, 213Future explorationsowner-decision, deferrednone
217-221Remediation report epicdeferred-by-decisionnone
223Bounded cursor pagination over IPCpreserved-candidatecore merged (PR #245); 07f175b holds IPC/preload wiring not on main; feat/issue-223-pagination-core @ 2f1cecd superseded (weaker cursor auth)retire 2f1cecd; port 07f175b post-v1
224, 226Evidence UI / exportdeferred-by-decisionnone
225Storage TLS evidence pilot (WAF SE:04)technically-blockedwaf-se-04 quarantinedafter #162
252CIS-CAT adoptionstalled (planning)docs merged (#264)fold into #4
253CIS-CAT parity and identity driftpreserved-candidatefeat/issue-253-parity-r2 @ 97d920f, 1 ahead / 54 behindrebase and review; pairs with #378
254-258Post-MVP PowerShell collectorsdeferred-by-decisionnone
373142-B orphan legacy rulespreserved-candidatefeat/issue-373-142b-orphan-rules @ 1a317e0rebase, review, merge
374142-C severity provenancepreserved-candidatefeat/issue-374-142c-severity-provenance @ 92d4e6crebase, review, merge
375142-D pending-unmapped 2.2.1/2.2.2owner-decisionpending-unmapped.json names both tuplesapprove or reject the two tuples
376142-E discharge the approval gateowner-actionneeds CIS_HUMAN_APPROVALS_JSON + SHA secretssupply, or move gate off push path
377142-F licensed re-extractionowner-actionneeds the two xlsx on an authorized machinerun extract-cis-corpus.py --check; record receipt
378142-G close #142 with residual-risk listtechnically-blockedon #373-#377last

5.3 The C01-C40 and WP-001 to WP-080 batch (#265-#371, 106 issues, all created 2026-09-17)​

What they are: a plan-level decomposition, produced in one sitting, of Azure collector expansion (C01-C11 storage and vault, C13-C25 monitoring and network, C27-C40 Defender, RBAC, Entra, Databricks), exact API-version projections for each provider call (WP-001 to WP-042), CIS 9.3.x bindings (WP-043 to WP-050), reconciliation of the #162 canonical-shape work (WP-051 to WP-054), the permission-ask derivation (WP-055 to WP-057), a request/receipt kernel and completeness certificates (WP-058 to WP-066), and a bounded evidence IPC pipeline (WP-067 to WP-080). None has a branch, worktree, or code on main. Dependency edges exist only in titles ("reuse #225 substrate", "with existing #162 candidate", "with the existing #171 gate").

Is the batch still the right plan? No, as issues. Three reasons:

  • Part of it is done. WP-051 (remove diagnostic-setting-as-activity-alert false pass) is implemented on main (evaluator-registry.ts:1301-1370) and quarantined behind #162. WP-052/053/054 describe #162's canonical shapes, which #162 already carries. WP-062 (RG/tag intersection) describes applyResourceScope behaviour that main already has (azure-collector.ts:608-642); whether it is a bug depends on a spec the issue does not cite.
  • Part of it is a different architecture. "Request kernel", "datapoint completeness certificates", "fact generations with atomic visible-generation switch" describe a pipeline that main does not have and that no v1 gate requires. They belong in one design issue with a spec, not 25 leaves.
  • Part of it is post-MVP by its own labels (WP-002 to WP-042, WP-048 to WP-050, WP-075/076, and every C-series issue is p3).

True dependency chain for v1 (what the batch was trying to express): #162 canonical shapes proven then un-quarantined via #375/#376, then #164 verified on a live tenant (#110), then WAF evaluator honesty (F4) and the #230 port, then #166 provenance, then reports. Everything else in the batch is post-v1.

Recommendation (§9.5): keep #351-#354 open only until #162 closes and mark them duplicates; convert C01-C40 plus WP-001 to WP-042 into three post-MVP epics; convert WP-058 to WP-080 into one design issue under #203; close WP-043 to WP-050 as duplicates of the #142/#375 work; keep WP-055/057 as sub-tasks of #146.

5.4 Closed-but-actionable​

#StateWhere the content livesResidual
171closed 2026-09-16PR #262 (a614a942) merged; main has abortable() in both collectors, discardUnpublishedSnapshot, commit-gated createSnapshot, cancelled-run retry tokenscandidate 7fc9fa68 (owner card residual 47 lines): the named features are on main; re-measure, retire as shipped
172closed 2026-09-16PR #263 (b1b715f9); diagnostics.ts on maincandidate 51678e85 referenced by the owner card sequences first; re-measure, retire
201closed 2026-09-15PR #260 (c2c2cf79); workloads column, validation, scoping on maincandidate 1fb4216e: 99.6% contained per the coordinator; retire as shipped
233closed 2026-09-15PR #246 cutover; PR #382 ported F1 (IncompleteDatabaseFileError on main, sqlcipher-adapter.ts:195-210)candidate bc9a5934 (unpushed sol/issue233-serial): same F1 subject; retire as shipped
372closed 2026-09-21CIS source-binding job green at mainnone

6. Recovery ledger (completed-then-stalled work)​

IssuePreserved SHA and branchWhat was completedWhat gates itDivergence from mainCheapest path back
#230 WAF Security pillarc4fcb028 on assessor/issue230-report-0498d3a1-r1 (worktree m365-assessor-issue230-report-0498d3a1-r1)independent APPROVE, 0 blocking findings; +751/-117, 10 filesmerge authority; two environment REDs (0600 checkout, /tmp node_modules) that reproduce on base8 commits behind, all PR #391 drift-guard files the candidate does not touchowner waives env REDs as provisioning defects, rebase onto c728ea48, rerun npm test in a correctly provisioned worktree, merge
#236 WP1 native build receipts42204a1 on issue236/wp1-native-build-receiptsWP1 receipt/install/load adapters; evidence card t_9518469cOQ-A/OQ-B rulings (owner card t_a64e1564); check-id ownership32 behind; WP3/WP4 merged since and touched the same inspectordispatch the matrix first; if install and node-electron-load report blocked, port 42204a1 onto main as the adapter for those two ids
#236 WP2 container probea33b2c2 on assessor/issue236-wp2-20260924T1737ZR2/R3/R4 closed in the container probe (plan ad6b86cf)same as WP132 behindsame as WP1, for container and asar-native-layout
#253 parity r297d920f on feat/issue-253-parity-r2canonical-binding drift guard and corpus parsernever reviewed after PR #261 landed54 behindrebase onto main; request one review; it is a test-only addition
#373 orphan rules1a317e0 on feat/issue-373-142b-orphan-rulesdispositions recorded for 20 orphan rulesreview54 behindrebase, review, merge
#374 severity provenance92d4e6c on feat/issue-374-142c-severity-provenancehonest severity provenance for 309 rulesreview54 behindrebase, review, merge
#142/#253 MFA Conditional Access accessor3abd165 on feat/issue142-mfa-accessor-r140 (and bdb95fc preserve branch); integration attempt 1a89c49 on cis/r141-pr261-mfa-integration (38 ahead)R140 hardened decoder after R135 reviewnever integrated after PR #261 merged from a different head; no decoder of this shape exists in main's registry57 behind; PR #261 lineage divergeddecide whether an MFA-via-CA evaluator is wanted for v1 (it would be a third automated M365 control); if yes, port 3abd165 alone, not the 38-commit integration branch
#223 IPC wiring07f175b on assessor/issue223-ipc-20260923T0045Znarrow IPC/preload/session wiring for the merged pagination coreowner card ruling; post-v131 behindpost-v1 port; small
#169 packaged integrity66beb61 on feat/issue169-overflow-r1main-owned allow-list preflight and transactional outputsuperseded in part by WP6 seam60 behindre-scope #169 to SBOM + dependency-review + secret-scan; take only the allow-list logic from the branch
#137, #139 mapping docs38b03c0, e129186control-to-endpoint mapping tablesSol final verification cards (todo, never run)77-92 behind; source-derived rules now carry permissionsdecide whether the docs still add value over the generated rule fields; if not, close and archive
Superseded, retire without review1fb4216e (#201), 2f1cecd (#223 core), 7fc9fa68 (#171), 51678e85 (#172), bc9a5934 (#233), 0310e36e (#235 WP4), ac82d6b1/b8fc7f1/083ef33/7c8778a/14232b6 (#236 WP3-B2 attempts), 6af144e (#162), 3620ca6 (#160), 6926adc (#147), c2ab0fe (#140), 683ed61/4ea90ca/b657963/1f0b009/6072d22/ef270af (#234), all cis/pr261-* and cis/lineage-recon-*, cis/authority-r2-f1f2, feat/waf-security-current-main-r1, feat/waf-manifest-remediation-r1each is either an exact merged-PR head or an intermediate of a merged lineagenonen/aarchive tags, delete branches after a content check

7. Kanban and WIP reconciliation​

7.1 The 323 blocked cards​

Counted from kanban-active-compact.json titles (approximate where titles are the only signal):

CategoryApprox. countVerdict
Mirrors of GitHub issues: "WP-0nn", "C0n", "GitHub #N"about 110not blockers; delete or make read-only mirrors
Issue #140 loop residue (Terra/Sol/DeepSeek rows, per-control continuations)about 45stale; #140 merged via PR #244
Issue #142 section-batch authoring cards and Unrated loopabout 20stale; PR #261 landed the full baseline
Issue #223 loop (cursor auth rounds)about 15stale; core merged, wiring is one candidate
PR #200 loopabout 12stale; superseded by PR #246
#236 WP3-B2 loop (plans r3-r8, four attempts, reviews)about 12stale since PR #391 (2026-09-28T03:24Z)
#230, #234, #235, #233, #171, #179 remediation loopsabout 30stale except the four candidates in §6
Fleet delivery program D0-D9 and "durable fleet delivery"11 (mixed todo/blocked)operations, not product
Owner-decision cards3real; see §8
Operations defect cards3real; see §7.2
Genuinely blocked candidate cards (WP1 evidence, WP1-5 plan, #253 continuation, #230 port, #373/#374/#375 rows, #171 RED reproduction)about 10real

Cross-check against the live census: no card that claims a live worker matches a live process. The census shows no opencode, codex, or implementer process; the only running assessor components are the lane controller and one planner one-shot. Every "IMPLEMENTATION … controller-launched, slot implementer-N" card (for example t_66c2384e, t_9a33dff8, t_9418d70e, t_2a11ad6c, t_fa69acf6) is not live. The ready card t_3dabad21 ("Assess PR261 as the R141 MFA integration vehicle") is stale: PR #261 merged 2026-09-20. The triage card t_d8a242c4 (#230 port plan) is superseded by the approved candidate c4fcb028.

7.2 Owner-decision and operations cards​

Three cards carry [OWNER DECISION] in the title; the brief's fourth is most likely t_8d7267cb ("Special owner-authorized Opus: implement complete 287-control CIS runtime baseline"), which is an authorization card, not a decision card, and is discharged by PR #261.

CardClaimStill true?
t_93b3147b D-1 (#230 c4fcb028)approved, awaiting merge authority, two env REDstrue; recommend waive REDs (both reproduce on base and are provisioning defects) and merge after rebase
D-2 (#235 WP4 0310e36e)approved, awaiting integrationlikely superseded: PR #380 head d6f1cb35 "register WP4 backup-proof helper in the #234 boundary allowlist" is the same subject as 0310e36e "reconcile WP4 helper-module allowlist with backup-proof helper", merged 09-22; verify by diff then retire
D-3 (#233 bc9a5934)review-complete, unpushed, parkedsuperseded: PR #382 ported F1 to main 09-25; retire as shipped
D-4 (#236 WP3-B2 attempt 5?)fourth rejection, threshold crossedstale 40 seconds after creation: card created 2026-09-28T03:23:22Z; PR #391 (WP3-B2 default-count lineage 5d4aec0d) merged 03:24:02Z. Refuse attempt 5 because the lane landed by another lineage; delete r7/r8 branches
t_a64e1564 (#236 WP1-WP5 plan, OQ-A to OQ-F)six rulings needed before any WP briefpartly overtaken: WP3-A/B1/B2 and WP4 merged, so check-id ownership for identity, container, asar-native-layout, cleanup, buildReceipt is decided by what landed. Rule "as landed on main"; the only live question is who owns install and node-electron-load (WP1 candidate) and supply-chain (WP5, absent)
t_6747ba12 (#201 1fb4216e, #223 2f1cecd, #171 7fc9fa68)three rulingsrecommend (a) retire-as-shipped for all three; #223's weaker cursor auth must not be reviewed; #171's named residual features are present on main
t_859c8243 OPERATIONS worktree provisioning (umask 0077 strips read bits; /tmp symlinked node_modules)manufactures false REDsconsistent with the two REDs on #230 and with dependency-policy-core.mjs refusing symlinked node_modules; I could not stat the worktrees, so "true" is inferred from the card's own ls-tree/stat evidence
t_5522d2fb OPERATIONS controller discards worker stdoutwriters die undiagnosableread of controller.py denied; unverified. The census shows no writers at all, which is consistent with the described silent-exit pattern but does not prove it
t_30168e43 planner orchestration repairoperations; not product

7.3 The canonical dirty checkout​

/root/prj-xis-m365_azure_assesor is on docs/three-layer-architecture @ dd11065 (the merged head of PR #259; 1 ahead by ancestry, 62 behind). Its working tree has 81 modified tracked files (+22,138/-706) and 60 untracked files.

Verdict: not lost work; it is main's content sitting on a stale branch. Evidence: every one of the 60 untracked paths exists in the pristine main tree (docs/CERTIFICATE-SETUP.md, docs/CIS-SOURCE-AUTHORITY.md, resources/cis-authority/, resources/cis-source-corpus/, resources/waf-baseline/, all 17 scripts/*.py and .mjs, src/main/rules/cis-*.test.ts, src/shared/waf-baseline-manifest.ts, test/fixtures/cis-preexisting-rules-baseline.json, and so on), and they are exactly the files added to main between dd11065 and c728ea48. The modified set (ci.yml, rule JSONs, control indexes, collectors, ipc.ts, db-service.ts, renderer components) is the set main changed in the same window. The diffstat magnitude (+22k lines, mostly rule JSON and control indexes) matches PR #261's baseline landing. I could not read the files to confirm byte equality, so this is an inference at high confidence. Cheapest resolution: git stash or git diff > backup.patch, git checkout main && git pull, then git diff --stat main -- . against the stash; if the residual is empty, drop it. Nothing in this tree should be committed on docs/three-layer-architecture.

7.4 Branches and worktrees​

83 branches ahead of main:

BucketCount (approx.)ExamplesAction
Exact heads of merged PRs (ahead only by ancestry)18opencode/issue-163 (#193), opencode/issue-167 (#196), claude/issue-165 (#194), opencode/issue-182 (#195), opencode/issue-133-recovery (#239), claude/issue-231 (#240), opencode/issue-136-recovery (#176), opencode/issue-132-collector (#178), opencode/issue-130-ui-copy (#177), opencode/issue-144-recovery (#159), claude/issue-145 (#155), claude/issue-190 (#192), feat/waf-strict-boundary-r2 (#251), docs/cis-cat-permission-role-contract (#264), docs/three-layer-architecture + 3 wt/t_* (#259), cis/pr261-committed-source-ci-r2 (#261), assessor/issue236-wp3b1-* (#384)delete after git log --oneline main..<branch> shows only the merged commits
Superseded intermediates of merged lineagesabout 409 cis/pr261-*, 3 cis/lineage-recon-*, cis/authority-r2-f1f2, 5 assessor/issue236-wp3b2-*, 6 feat/issue-234-*, sol/issue233-serial, feat/issue-235-wp4, claude/issue-222, claude/issue-232, claude/pr200-*, claude/issue-199-*, opencode/issue-140, claude/issue-162, opencode/issue-160, opencode/issue-147, opencode/issue-171-cancellation, opencode/issue-172-diagnostics, claude/issue-201, feat/issue-223-pagination-core, feat/waf-*-r1, claude/issue-207tag archive/<branch> then delete
Preserved candidates worth keeping (§6)10c4fcb028, 42204a1, a33b2c2, 97d920f, 1a317e0, 92d4e6c, 3abd165, 07f175b, 66beb61, 38b03c0/e129186keep, rebase when scheduled
Post-MVP feature candidates6opencode/issue-94-palette-core, claude/issue-175, feat/issue179-backlog-currentmain, claude/issue-188, opencode/issue-173, claude/issue-134tag and delete; the tag preserves them

316 worktrees: 1 canonical, 1 this review's pristine main, about 85 under /root/Working/Projects (issue worktrees), about 200 under /root/Working/Reviews (detached review checkouts, of which about 45 are #140 rounds alone), 10 under /root/Working/Operations, 5 under .worktrees, 2 under /tmp. Every detached review worktree is disposable once its review report is archived. Estimated cleanup scope: about 290 worktrees removable (git worktree remove or prune after deleting directories), about 60 branches deletable after tagging, leaving about 10 candidate branches and their worktrees.


8. What requires the owner now​

Decisions​

IdDecisionEvidence it is readyWhat resumes
D-1Grant merge authority for #230 c4fcb028; waive the two environment REDs as provisioning defectsindependent APPROVE recorded; REDs reproduce on base; scripts/ untouchedWAF Security pillar lands; README counts re-derived
D-2Retire 1fb4216e (#201), 2f1cecd (#223), 7fc9fa68 (#171), 51678e85 (#172), bc9a5934 (#233), 0310e36e (#235 WP4) as shipped-on-main after a one-line re-measure each§5.4 and §6two writer slots free; owner cards close
D-3Refuse a fifth #236 WP3-B2 attempt; delete r7/r8 branchesPR #391 landed the lane 2026-09-28T03:24ZWP3-B2 closed
D-4Rule OQ-A to OQ-F "as landed on main"; assign install/node-electron-load to the WP1 candidate and supply-chain to a new WP5WP3 and WP4 mergedWP1/WP2 port can be briefed
D-5#375: approve or reject the two pending Azure tuples (cis-azure-2.2.1, 2.2.2)tuples and fingerprints in resources/cis-authority/pending-unmapped.jsonthe approval file can be written
D-6#161: v1 permission ask is the five collector-exercised Graph permissions plus Reader (current code and README), or the rule-declared setREADME, permissions-table.ts, rule permissions fields#146, #170 chain unblocks or closes
D-7v1 report: pdfkit plus provenance (F6) now, and the report standard (#208-#210) after v1; or #208 in v1template and data layer on main, untested end to end#227 epic scope fixed
D-8Ship v1 unsigned for internal use (README already says so) or fund certificates (#111)docs/SIGNING.md#111 closes or schedules
D-9CIS approval gate: supply approvals and workbook secrets, or move cis-source-authority to workflow_dispatch so push CI can be greenci.yml:202-331main CI becomes a regression signal
D-10Retire the C/WP batch into epics per §5.3none of the 106 has codeboard and issue list shrink by about 100

Actions​

IdActionOwner
A-1Dispatch gh workflow run ci.yml --ref main; collect the four verdicts and the NSIS/DMG artifactsowner or release engineer
A-2Write CIS_HUMAN_APPROVALS_JSON naming the three subjects, record its SHA-256 out of band, set both as repository secrets (after D-5)owner, on an authorized machine
A-3Run extract-cis-corpus.py --check against the two licensed workbooks on an authorized machine; optionally set CIS_LICENSED_WORKBOOKS_B64 (#377)owner
A-4Fix worktree provisioning (umask 022, copy node_modules, post-provision assertion) and persist worker stdout before dispatching any writeroperations owner
A-5Human gates in order: #109 smoke test on macOS and Windows, #237 NSIS lifecycle, #110 live tenantthe consultant with a laptop and a tenant

Human gates later​

#110 needs a customer or lab tenant with the five Graph permissions and Reader; #238 needs #237's evidence; #111 only if D-8 chooses signing.

Resolved asks (no action needed)​

The CIS 287-control baseline (t_8d7267cb authorization), the SQLCipher cutover escalation (#233 F1), the WAF denominator pin (#251), workload selection (#201), cancellation and retry (#171), diagnostics (#172), and the encrypted-DB proof stack WP1-WP4 (#235) are on main.


9. Forward plan to complete v1​

9.1 Definition of Done for v1​

Derived from README "Current limitations", plans/00-context.md, epics #3/#4/#115/#227 and the mvp-critical label. v1 is done when every line below is true and recorded.

  1. A Windows x64 NSIS installer and macOS x64/arm64 DMGs are produced by the ci.yml native matrix from a main SHA, with the four verdict.json artifacts attached to the release (#3, #236).
  2. SECURITY.md §9 checklist executed and recorded on Windows and macOS from those installers, including: window opens under sandbox: true; bundled rules present; delete-after-restart leaves no data; secret store entries created and removed; IPC rejects a bad UUID (#109).
  3. One live tenant assessed end to end with the documented permission set: Global Administrator count non-zero; a run without AuditLog.Read.All reports error, not a finding; a tenant with more than 999 users pages correctly (#110).
  4. Windows encrypted-data lifecycle validated: create, collect, close app, reopen, delete, no plaintext SQLite format 3 header in userData (#237).
  5. Every unquarantined automated evaluator returns error or not-assessed, never pass or fail, when its input collection failed or is absent (F2, F4 fixed; azure-evaluator-correctness.test.ts covers 4.1.1, 4.1.2, waf-se-05, waf-oe-02).
  6. Assessment refuses to run with zero loaded rules or any rule-file load error; a packaged build with an empty resources/rules fails visibly (#168, F5).
  7. Every PDF, Excel, PPTX, JSON and briefing states benchmark versions, rulesVersion, rulesHash, app version and snapshot id (#166, F6), drift-tested.
  8. Engagement close, reopen, sign-off and a retention countdown are reachable in the UI; a DOM test proves the channel is invoked; DATA-HANDLING.md §6.2 is true (#118, F1).
  9. CIS: #373, #374 merged; #375 ruled; approval evidence supplied or the gate reclassified (#376); licensed re-extraction receipt recorded (#377); #142 closed with a residual-risk list (#378). README counts re-derived by the drift test.
  10. WAF: #230 Security pillar merged; waf.json sectionRef values corrected to the manifest's itemId or the rule file states they are historical; README WAF counts re-derived.
  11. #160 and #234 verified against main and closed; #229 and #235 closed.
  12. Docs reconciled: DATA-HANDLING resource lists (F10), BUILD-AND-TEST prerequisites (F11), SECURITY §4 statements (F17), plans/00-context.md counts.
  13. Push CI on main is green (D-9), and the release SHA's run is green end to end.
  14. Owner has ruled D-6, D-7, D-8 and the outcomes are reflected in README.

9.2 Critical path​

Each step: role, inputs, action, expected output, verification, stop condition, dependency.

#StepRoleInputsOutputVerificationStop whenDepends on
1Dispatch native matrixrelease engineermain c728ea484 distributables, 4 verdicts, aggregatenative-package-aggregate artifact present; list blocked check idsartifacts downloadednone
2Decide CIS gate (D-9) and applyowner + one engineerci.yml, secretseither secrets set or job moved to dispatchnext push to main is greengreen run observednone
3Fail-closed evaluator fix (F2, F4)implementerevaluator-registry.ts, correctness testsPR with 4 evaluators hardened + testsnpm test; coverage-counts drift test still green (counts may change; README updated in the same PR)mergednone
4Merge #230 portimplementer + reviewerc4fcb028, correctly provisioned worktreerebased PRfull suite green in a 0644-mode worktreemergedD-1, A-4
5Provenance in exports (F6)implementerexport-service.ts, engine statusone provenance helper, four exporters, drift testtest proves each format contains rulesHashmergednone (conflicts with step 3 only in README)
6Zero-rules fail closed (F5)implementerindex.ts, ipc.tsstartup fatal on packaged build without rules; assessment:run refusesunit tests for bothmergednone
7Retention UI (#118, F1)implementer (renderer)ipc-channels.ts, ProjectWorkspace.tsxengagement panel + DOM tests; DATA-HANDLING §6.2 amendedDOM test invokes each channel; manual check in devmergednone
8CIS closure chainimplementer + reviewer + owner1a317e0, 92d4e6c, 97d920f, D-5, A-2, A-3#373, #374, #253 merged; approvals file; receiptcheck:cis-approval green locally with the supplied files#378 closedD-5, A-2, A-3
9Human gatesconsultantinstallers from step 1 (rebuilt after steps 3-7)filled §9 checklist, live-tenant report, Windows lifecycle logevidence attached to #109/#110/#237all three pass1, 3, 6
10Report decision (D-7) and, if chosen, #208owner then implementertemplate, report-data.ts, F8 fixprintToPDF exporter behind export:pdfgolden-file test; manual rendermerged or deferredD-7
11Docs reconciliation (F10, F11, F17)technical writer or implementerfindings listone docs PR with drift tests where cheaptests greenmerged3, 5, 7
12Board and branch hygiene (§9.5)coordinatorthis reportclosed cards, tagged and deleted branches, pruned worktreesboard shows about 10 blockeddoneD-2, D-3, D-10
13Releaseownergreen main run, human evidencetagged v1 release with artifacts and verdictsDoD list checkedreleasedall

9.3 Parallel lanes (no file-conflict risk)​

  • Lane A, evidence: steps 1, 2, 9. Touches no source files.
  • Lane B, compliance core: steps 3, 5, 6. Files: evaluator-registry.ts, azure-evaluator-correctness.test.ts, export-service.ts, index.ts, ipc.ts (assessment handler only), README counts. One implementer, sequential inside the lane.
  • Lane C, retention UI: step 7. Files: src/renderer/**, docs/DATA-HANDLING.md §6.2 only. No overlap with B.
  • Lane D, CIS closure: step 8. Files: resources/control-indexes/*, resources/cis-authority/*, src/main/rules/control-index-drift.test.ts, rule JSON severity fields. Overlaps B only if B quarantines or un-quarantines rules; sequence: B first (quarantine list unchanged), D second.
  • Lane E, WAF: step 4. Files per the candidate (WAF rules, manifest, correspondence, reports). Overlaps B in evaluator-registry.ts only if the candidate edits WAF evaluators; the card says it re-ports "Security SE:06-SE:12 availability", which touches waf.json and the manifest, so merge B's F4 fix first and rebase E.
  • Lane F, hygiene: step 12. No source files.

9.4 Unblock plan​

ItemExact decision or actReady evidenceResumes
CIS gate redD-9. Recommended: keep cis-source-authority as a required job only on workflow_dispatch and on a release/* branch; keep cis-source-binding and cis-authority (committed-bytes) on push. Rationale: the gate is human-only by design and cannot be satisfied by codeci.yml:202-331; failure logpush CI becomes a regression signal; release still requires the gate
#375approve tuples d2586e1c… and 10e5cd71… or reject with reasonpending-unmapped.jsonapprovals file can name them
#376write approvals JSON with approvedSubjects for the two rules and sourceDerived, each with sourceBindingSha256, approver, approvedAt; record SHA-256 out of band; set two secretsdocs/CIS-SOURCE-AUTHORITY.md:266-285check:cis-approval passes
#377run the licensed --check on an authorized machine; commit nothing; record the receipt path in #377extractor and workbooks#378
#161D-6current five-permission ask#146, #170 chain
#208D-7template on main#209, #210
#111D-8SIGNING.mdclose or schedule
#238after #237 evidence: adopt, or roll back to the legacy harness pathnone yet#229 epic closes
Owner cardsD-1 to D-4§7.2four writer slots free
Worktree REDsA-4operations cardcandidate gates can be trusted again

9.5 Board hygiene plan (list only; nothing closed by this review)​

Close as stale on the board (about 250 cards): every "GitHub #N" mirror whose issue is closed or deferred; every #140, #142-section, #223-loop, PR #200, #236-WP3-B2 (r3-r8 plans, attempts 1-4, reviews), #234-r1..r5, #233, #171, #179, #94 loop card; the ready card t_3dabad21; the triage card t_d8a242c4; all [assessor] Issue #N delivery → Terra review and Sol final verification pairs for merged issues (#140, #147, #162, #166 remains open, #169 remains open, #173, #179, #180, #181, #199, #207, #118 remains open).

Merge into fewer real work packages: the ten §6 candidates become ten cards, one each, with the SHA in the title and nothing else; the D0-D9 fleet program moves to an operations board; the three owner-decision cards collapse into one card listing D-1 to D-10 with checkboxes.

Propose closing on GitHub (owner to confirm): #140, #147, #160, #199 as complete-by-content; #183 after a Cancel-button check; #351 as implemented and duplicate of #162; #352-#354, #343-#347 as duplicates of #162/#375; #265-#300, #302-#342, #348-#350, #370-#371 folded into three post-MVP epics; #355-#369 folded into one design issue under #203; #252 folded into #4; #229 once #238 is decided; #234 and #235 after the diff checks in §6.

9.6 Risks and evidence gaps​

Covered in §10.

  1. Rule on t_6747ba12 and t_93b3147b using D-1 to D-4: retire six candidates as shipped, approve the #230 merge with the environment REDs waived, refuse a fifth WP3-B2 attempt.
  2. Dispatch ci.yml on main and read the four native verdicts; this is the only way to learn what WP1/WP2/WP5 still owe.
  3. Decide D-9 and either set the CIS approval secrets or move the gate off the push path, so tomorrow's pushes are green or red for a reason.
  4. Open two ready issues with the file lists from §9.3: "fail-closed on collection failure for cis-azure-4.1.1/4.1.2, waf-se-05, waf-oe-02" and "#118 engagement lifecycle UI"; both are one-day bounded changes with existing tests to extend.
  5. Fix worktree provisioning (umask 022, copied node_modules, post-provision assertion) before any writer is dispatched; until then every full-suite RED is uninterpretable.

10. Risks and evidence gaps​

Risk or gapWhy it mattersWhat closes it
No packaged run ever observed (sandbox: true, fuses, rules bundled, secret store on Windows/macOS)every "fixed" in the docs is unobserved on the platforms consultants usesteps 1 and 9
No live-tenant runH2/H3/H4 fixes (role paging, error-versus-negative, Graph paging) unproven against Microsoft's real shapes; #162's quarantine can only be lifted with such data#110
Windows native driver behaviourprebuild SHA pinned, but load-in-Electron on win32 not observed; sqlcipher-adapter profile read-back could differ#237; native matrix node-electron-load check
Push CI red by designmasks regressions; the coordinator already reads per-job results, humans may notD-9
Test suite depends on Python and file modesfalse REDs consumed reviewer rounds on #230; a clean laptop fails the runbookF11, A-4
Aggregate job id mismatch (ci.yml:658-661 literals vs policy-derived matrix.jobId)a first dispatch may fail in the aggregate for a bookkeeping reasoninspect native-package-matrix.mjs output on the first dispatch
No SBOM producer despite the inspector requestsupply-chain check reports blocked; #169 openWP5 or re-scoped #169
Unrated severity on 277 rulesreports and backlog priority treat unrated as a distinct bucket; the DevOps adapter maps no priority for it (azure-devops-adapter.ts:27-33), which is correct but means most exported work items carry no prioritydocument in README backlog section; owner decides whether a default priority is wanted
Legacy pdfkit report and WinAnsi fontsnon-Latin customer names render as boxesD-7
Board state divergence323 blocked cards with about 10 real blockers; decisions written in reconciliation reports rather than cards§9.5
The MFA Conditional Access accessor lineage38-commit integration branch diverged from what merged; may hold an evaluator that was reviewed to R140 and then dropped§6 decision on 3abd165
I could not read the canonical checkout or the lane controllerthe dirty-tree verdict and the operations card are inferredone git stash and one read of controller.py:583 by someone with access

11. Appendix​

11.1 Evidence paths used​

All under /root/Working/Reviews/assessor-full-portfolio-fable-20260928/:

  • inputs/brief.md, inputs/manifest.json
  • evidence/main-c728ea48e71674e8e4b052b7b16d5933a32a080f/ (pristine main): README.md, SECURITY.md, CLAUDE.md, HANDOFF.md, REVIEW.md, package.json, plans/00-context.md, plans/01-work-tracking.md, plans/AGENTS.md, plans/COORDINATOR.md, docs/ARCHITECTURE.md, docs/BUILD-AND-TEST.md, docs/DATA-HANDLING.md, docs/CIS-SOURCE-AUTHORITY.md, docs/waf-baseline-reconciliation.md, docs/SIGNING.md, .github/workflows/ci.yml, .github/workflows/windows-build.yml, src/main/index.ts, src/main/ipc.ts, src/preload/index.ts, src/shared/ipc-channels.ts, src/shared/coverage.ts, src/main/db-service.ts, src/main/sqlcipher-adapter.ts, src/main/key-vault-service.ts, src/main/retention-service.ts, src/main/engagement-folder.ts, src/main/snapshot-portability-service.ts, src/main/export-service.ts (first 700 lines), src/main/report/report-data.ts (first 400 lines), src/main/report/template.ts (first 200 lines), src/main/github-adapter.ts, src/main/azure-devops-adapter.ts, src/main/curation-service.ts (grep), src/main/collector/collector-service.ts, src/main/collector/azure-collector.ts, src/main/collector/m365-collector.ts, src/main/rules/rules-engine.ts, src/main/rules/rules-loader.ts, src/main/rules/rules-engine-singleton.ts, src/main/rules/types.ts, src/main/rules/evaluator-registry.ts, src/main/rules/coverage-counts.test.ts, src/main/rules/cis-quarantine.test.ts, resources/rules/*.json, resources/cis-authority/pending-unmapped.json, resources/cis-coverage-matrix.json (grep), scripts/dependency-policy-check.mjs, scripts/dependency-policy-core.mjs, scripts/audit-check.mjs, scripts/native-package-policy.json, scripts/inspect-native-package.mjs (first 150 lines), scripts/native-package-workflow-drift.mjs (lines 150-268), scripts/sqlcipher-legacy-harness/package.json (grep), src/renderer/** (grep only)
  • evidence/issues-compact.txt, open-issues-full.json, closed-issues-full.json, prs-closed-all.json
  • evidence/kanban-active-compact.json, kanban-all-active.json (grep and five card bodies)
  • evidence/wip-branch-census.json, worktree-list.txt
  • evidence/canonical-dirty-status.txt, canonical-checkout-status.txt, canonical-dirty-diffstat-full.txt, canonical-untracked.txt, canonical-ahead-commits.txt
  • evidence/ci-main-jobs.json, ci-main-failure-log.txt, actions-runs-40.json
  • evidence/main-log-40.txt, main-merges-30.txt, main-tree-files.txt
  • evidence/live-process-census.txt

11.2 Commands used​

Read-only tool calls only: Read on the files above, Grep for the counts and cross-references cited (quarantine markers, fn evaluator references, JMESPath rules, rule ids, it(/test( cases, renderReportHtml/toReportData/printToPDF callers, engagement-lifecycle channel callers in the renderer, toolAutomated/toolingGap totals, owner-decision card titles, done-card count), Glob for .github/**. No git command, no test run, no install, no write outside this file.

11.3 Unavailable evidence​

  • /root/prj-xis-m365_azure_assesor/** (read denied): canonical checkout content.
  • /root/Working/Operations/assessor-lane-controller/** (read denied): controller.py and any worker logs.
  • Job logs for main CI runs before 2026-09-28 (only conclusions were captured).
  • Output of any native package dispatch (none has run).
  • kanban-all-active.json done-card bodies were not read individually (481 cards); the recovery ledger relies on branch, PR and owner-card evidence instead.
  • Byte-level diffs between candidate SHAs and main.

FABLE-REVIEW-COMPLETE