wingfoil 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +334 -0
- package/dist/cli/error.d.ts +11 -0
- package/dist/cli/error.d.ts.map +1 -0
- package/dist/cli/error.js +27 -0
- package/dist/cli/error.js.map +1 -0
- package/dist/cli/exit.d.ts +19 -0
- package/dist/cli/exit.d.ts.map +1 -0
- package/dist/cli/exit.js +14 -0
- package/dist/cli/exit.js.map +1 -0
- package/dist/cli/index.d.ts +28 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +36 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init-command.d.ts +34 -0
- package/dist/cli/init-command.d.ts.map +1 -0
- package/dist/cli/init-command.js +100 -0
- package/dist/cli/init-command.js.map +1 -0
- package/dist/cli/mcp-command.d.ts +24 -0
- package/dist/cli/mcp-command.d.ts.map +1 -0
- package/dist/cli/mcp-command.js +42 -0
- package/dist/cli/mcp-command.js.map +1 -0
- package/dist/cli/output.d.ts +13 -0
- package/dist/cli/output.d.ts.map +1 -0
- package/dist/cli/output.js +28 -0
- package/dist/cli/output.js.map +1 -0
- package/dist/cli/program.d.ts +12 -0
- package/dist/cli/program.d.ts.map +1 -0
- package/dist/cli/program.js +279 -0
- package/dist/cli/program.js.map +1 -0
- package/dist/cli/registrar.d.ts +69 -0
- package/dist/cli/registrar.d.ts.map +1 -0
- package/dist/cli/registrar.js +82 -0
- package/dist/cli/registrar.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +45 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/approval-authority.d.ts +83 -0
- package/dist/core/approval-authority.d.ts.map +1 -0
- package/dist/core/approval-authority.js +112 -0
- package/dist/core/approval-authority.js.map +1 -0
- package/dist/core/builtin-asset.d.ts +71 -0
- package/dist/core/builtin-asset.d.ts.map +1 -0
- package/dist/core/builtin-asset.js +137 -0
- package/dist/core/builtin-asset.js.map +1 -0
- package/dist/core/builtin-integrity.d.ts +32 -0
- package/dist/core/builtin-integrity.d.ts.map +1 -0
- package/dist/core/builtin-integrity.js +148 -0
- package/dist/core/builtin-integrity.js.map +1 -0
- package/dist/core/confinement.d.ts +57 -0
- package/dist/core/confinement.d.ts.map +1 -0
- package/dist/core/confinement.js +108 -0
- package/dist/core/confinement.js.map +1 -0
- package/dist/core/context.d.ts +187 -0
- package/dist/core/context.d.ts.map +1 -0
- package/dist/core/context.js +192 -0
- package/dist/core/context.js.map +1 -0
- package/dist/core/directive-assign.d.ts +131 -0
- package/dist/core/directive-assign.d.ts.map +1 -0
- package/dist/core/directive-assign.js +352 -0
- package/dist/core/directive-assign.js.map +1 -0
- package/dist/core/directives-list.d.ts +80 -0
- package/dist/core/directives-list.d.ts.map +1 -0
- package/dist/core/directives-list.js +148 -0
- package/dist/core/directives-list.js.map +1 -0
- package/dist/core/exit-code.d.ts +85 -0
- package/dist/core/exit-code.d.ts.map +1 -0
- package/dist/core/exit-code.js +163 -0
- package/dist/core/exit-code.js.map +1 -0
- package/dist/core/git-identity.d.ts +32 -0
- package/dist/core/git-identity.d.ts.map +1 -0
- package/dist/core/git-identity.js +72 -0
- package/dist/core/git-identity.js.map +1 -0
- package/dist/core/index.d.ts +388 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +1331 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/init.d.ts +78 -0
- package/dist/core/init.d.ts.map +1 -0
- package/dist/core/init.js +167 -0
- package/dist/core/init.js.map +1 -0
- package/dist/core/loaders.d.ts +174 -0
- package/dist/core/loaders.d.ts.map +1 -0
- package/dist/core/loaders.js +364 -0
- package/dist/core/loaders.js.map +1 -0
- package/dist/core/memory-add-type.d.ts +43 -0
- package/dist/core/memory-add-type.d.ts.map +1 -0
- package/dist/core/memory-add-type.js +151 -0
- package/dist/core/memory-add-type.js.map +1 -0
- package/dist/core/memory-transition.d.ts +111 -0
- package/dist/core/memory-transition.d.ts.map +1 -0
- package/dist/core/memory-transition.js +269 -0
- package/dist/core/memory-transition.js.map +1 -0
- package/dist/core/registry.d.ts +189 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +66 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/relevance.d.ts +86 -0
- package/dist/core/relevance.d.ts.map +1 -0
- package/dist/core/relevance.js +258 -0
- package/dist/core/relevance.js.map +1 -0
- package/dist/core/require-reason.d.ts +27 -0
- package/dist/core/require-reason.d.ts.map +1 -0
- package/dist/core/require-reason.js +95 -0
- package/dist/core/require-reason.js.map +1 -0
- package/dist/core/types.d.ts +45 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +22 -0
- package/dist/core/types.js.map +1 -0
- package/dist/core/usage-error.d.ts +20 -0
- package/dist/core/usage-error.d.ts.map +1 -0
- package/dist/core/usage-error.js +29 -0
- package/dist/core/usage-error.js.map +1 -0
- package/dist/core/write-guard.d.ts +99 -0
- package/dist/core/write-guard.d.ts.map +1 -0
- package/dist/core/write-guard.js +247 -0
- package/dist/core/write-guard.js.map +1 -0
- package/dist/directives/create.d.ts +47 -0
- package/dist/directives/create.d.ts.map +1 -0
- package/dist/directives/create.js +82 -0
- package/dist/directives/create.js.map +1 -0
- package/dist/directives/index.d.ts +20 -0
- package/dist/directives/index.d.ts.map +1 -0
- package/dist/directives/index.js +32 -0
- package/dist/directives/index.js.map +1 -0
- package/dist/directives/roles-edit.d.ts +53 -0
- package/dist/directives/roles-edit.d.ts.map +1 -0
- package/dist/directives/roles-edit.js +286 -0
- package/dist/directives/roles-edit.js.map +1 -0
- package/dist/directives/schema.d.ts +65 -0
- package/dist/directives/schema.d.ts.map +1 -0
- package/dist/directives/schema.js +68 -0
- package/dist/directives/schema.js.map +1 -0
- package/dist/dna/edit.d.ts +61 -0
- package/dist/dna/edit.d.ts.map +1 -0
- package/dist/dna/edit.js +429 -0
- package/dist/dna/edit.js.map +1 -0
- package/dist/dna/index.d.ts +24 -0
- package/dist/dna/index.d.ts.map +1 -0
- package/dist/dna/index.js +59 -0
- package/dist/dna/index.js.map +1 -0
- package/dist/dna/mutate.d.ts +74 -0
- package/dist/dna/mutate.d.ts.map +1 -0
- package/dist/dna/mutate.js +318 -0
- package/dist/dna/mutate.js.map +1 -0
- package/dist/dna/path.d.ts +175 -0
- package/dist/dna/path.d.ts.map +1 -0
- package/dist/dna/path.js +289 -0
- package/dist/dna/path.js.map +1 -0
- package/dist/dna/roles.d.ts +57 -0
- package/dist/dna/roles.d.ts.map +1 -0
- package/dist/dna/roles.js +50 -0
- package/dist/dna/roles.js.map +1 -0
- package/dist/dna/schema.d.ts +172 -0
- package/dist/dna/schema.d.ts.map +1 -0
- package/dist/dna/schema.js +182 -0
- package/dist/dna/schema.js.map +1 -0
- package/dist/dna/set.d.ts +145 -0
- package/dist/dna/set.d.ts.map +1 -0
- package/dist/dna/set.js +428 -0
- package/dist/dna/set.js.map +1 -0
- package/dist/mcp/dna-resource.d.ts +16 -0
- package/dist/mcp/dna-resource.d.ts.map +1 -0
- package/dist/mcp/dna-resource.js +56 -0
- package/dist/mcp/dna-resource.js.map +1 -0
- package/dist/mcp/index.d.ts +49 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +60 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/memory-resource.d.ts +17 -0
- package/dist/mcp/memory-resource.d.ts.map +1 -0
- package/dist/mcp/memory-resource.js +133 -0
- package/dist/mcp/memory-resource.js.map +1 -0
- package/dist/mcp/prompt.d.ts +48 -0
- package/dist/mcp/prompt.d.ts.map +1 -0
- package/dist/mcp/prompt.js +176 -0
- package/dist/mcp/prompt.js.map +1 -0
- package/dist/mcp/read-only.d.ts +42 -0
- package/dist/mcp/read-only.d.ts.map +1 -0
- package/dist/mcp/read-only.js +93 -0
- package/dist/mcp/read-only.js.map +1 -0
- package/dist/mcp/registrar.d.ts +54 -0
- package/dist/mcp/registrar.d.ts.map +1 -0
- package/dist/mcp/registrar.js +72 -0
- package/dist/mcp/registrar.js.map +1 -0
- package/dist/mcp/server.d.ts +58 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +64 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/workflow-resource.d.ts +17 -0
- package/dist/mcp/workflow-resource.d.ts.map +1 -0
- package/dist/mcp/workflow-resource.js +63 -0
- package/dist/mcp/workflow-resource.js.map +1 -0
- package/dist/memory/add.d.ts +50 -0
- package/dist/memory/add.d.ts.map +1 -0
- package/dist/memory/add.js +131 -0
- package/dist/memory/add.js.map +1 -0
- package/dist/memory/audit.d.ts +177 -0
- package/dist/memory/audit.d.ts.map +1 -0
- package/dist/memory/audit.js +287 -0
- package/dist/memory/audit.js.map +1 -0
- package/dist/memory/commit-message.d.ts +146 -0
- package/dist/memory/commit-message.d.ts.map +1 -0
- package/dist/memory/commit-message.js +203 -0
- package/dist/memory/commit-message.js.map +1 -0
- package/dist/memory/entry.d.ts +56 -0
- package/dist/memory/entry.d.ts.map +1 -0
- package/dist/memory/entry.js +28 -0
- package/dist/memory/entry.js.map +1 -0
- package/dist/memory/frontmatter-edit.d.ts +68 -0
- package/dist/memory/frontmatter-edit.d.ts.map +1 -0
- package/dist/memory/frontmatter-edit.js +333 -0
- package/dist/memory/frontmatter-edit.js.map +1 -0
- package/dist/memory/git-log.d.ts +32 -0
- package/dist/memory/git-log.d.ts.map +1 -0
- package/dist/memory/git-log.js +100 -0
- package/dist/memory/git-log.js.map +1 -0
- package/dist/memory/history.d.ts +142 -0
- package/dist/memory/history.d.ts.map +1 -0
- package/dist/memory/history.js +250 -0
- package/dist/memory/history.js.map +1 -0
- package/dist/memory/index.d.ts +58 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +107 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/query.d.ts +150 -0
- package/dist/memory/query.d.ts.map +1 -0
- package/dist/memory/query.js +307 -0
- package/dist/memory/query.js.map +1 -0
- package/dist/memory/reject.d.ts +11 -0
- package/dist/memory/reject.d.ts.map +1 -0
- package/dist/memory/reject.js +32 -0
- package/dist/memory/reject.js.map +1 -0
- package/dist/memory/schema.d.ts +104 -0
- package/dist/memory/schema.d.ts.map +1 -0
- package/dist/memory/schema.js +161 -0
- package/dist/memory/schema.js.map +1 -0
- package/dist/memory/state-machine.d.ts +166 -0
- package/dist/memory/state-machine.d.ts.map +1 -0
- package/dist/memory/state-machine.js +431 -0
- package/dist/memory/state-machine.js.map +1 -0
- package/dist/memory/submit.d.ts +11 -0
- package/dist/memory/submit.d.ts.map +1 -0
- package/dist/memory/submit.js +44 -0
- package/dist/memory/submit.js.map +1 -0
- package/dist/storage/builtin-directives.d.ts +54 -0
- package/dist/storage/builtin-directives.d.ts.map +1 -0
- package/dist/storage/builtin-directives.js +152 -0
- package/dist/storage/builtin-directives.js.map +1 -0
- package/dist/storage/commit.d.ts +92 -0
- package/dist/storage/commit.d.ts.map +1 -0
- package/dist/storage/commit.js +201 -0
- package/dist/storage/commit.js.map +1 -0
- package/dist/storage/confinement.d.ts +93 -0
- package/dist/storage/confinement.d.ts.map +1 -0
- package/dist/storage/confinement.js +164 -0
- package/dist/storage/confinement.js.map +1 -0
- package/dist/storage/document.d.ts +27 -0
- package/dist/storage/document.d.ts.map +1 -0
- package/dist/storage/document.js +52 -0
- package/dist/storage/document.js.map +1 -0
- package/dist/storage/errors.d.ts +36 -0
- package/dist/storage/errors.d.ts.map +1 -0
- package/dist/storage/errors.js +46 -0
- package/dist/storage/errors.js.map +1 -0
- package/dist/storage/frontmatter.d.ts +27 -0
- package/dist/storage/frontmatter.d.ts.map +1 -0
- package/dist/storage/frontmatter.js +37 -0
- package/dist/storage/frontmatter.js.map +1 -0
- package/dist/storage/git-root.d.ts +17 -0
- package/dist/storage/git-root.d.ts.map +1 -0
- package/dist/storage/git-root.js +49 -0
- package/dist/storage/git-root.js.map +1 -0
- package/dist/storage/index.d.ts +29 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +66 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/init-state.d.ts +9 -0
- package/dist/storage/init-state.d.ts.map +1 -0
- package/dist/storage/init-state.js +24 -0
- package/dist/storage/init-state.js.map +1 -0
- package/dist/storage/layout.d.ts +51 -0
- package/dist/storage/layout.d.ts.map +1 -0
- package/dist/storage/layout.js +88 -0
- package/dist/storage/layout.js.map +1 -0
- package/dist/storage/memory-path.d.ts +72 -0
- package/dist/storage/memory-path.d.ts.map +1 -0
- package/dist/storage/memory-path.js +127 -0
- package/dist/storage/memory-path.js.map +1 -0
- package/dist/storage/snapshot.d.ts +16 -0
- package/dist/storage/snapshot.d.ts.map +1 -0
- package/dist/storage/snapshot.js +72 -0
- package/dist/storage/snapshot.js.map +1 -0
- package/dist/storage/templates.d.ts +88 -0
- package/dist/storage/templates.d.ts.map +1 -0
- package/dist/storage/templates.js +422 -0
- package/dist/storage/templates.js.map +1 -0
- package/dist/validation/error-mapper.d.ts +16 -0
- package/dist/validation/error-mapper.d.ts.map +1 -0
- package/dist/validation/error-mapper.js +18 -0
- package/dist/validation/error-mapper.js.map +1 -0
- package/dist/validation/errors.d.ts +69 -0
- package/dist/validation/errors.d.ts.map +1 -0
- package/dist/validation/errors.js +69 -0
- package/dist/validation/errors.js.map +1 -0
- package/dist/validation/id.d.ts +16 -0
- package/dist/validation/id.d.ts.map +1 -0
- package/dist/validation/id.js +177 -0
- package/dist/validation/id.js.map +1 -0
- package/dist/validation/index.d.ts +18 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +37 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/validation/secret-scan.d.ts +129 -0
- package/dist/validation/secret-scan.d.ts.map +1 -0
- package/dist/validation/secret-scan.js +367 -0
- package/dist/validation/secret-scan.js.map +1 -0
- package/dist/validation/two-pass.d.ts +24 -0
- package/dist/validation/two-pass.d.ts.map +1 -0
- package/dist/validation/two-pass.js +24 -0
- package/dist/validation/two-pass.js.map +1 -0
- package/dist/validation/warning.d.ts +31 -0
- package/dist/validation/warning.d.ts.map +1 -0
- package/dist/validation/warning.js +81 -0
- package/dist/validation/warning.js.map +1 -0
- package/dist/validation/yaml.d.ts +9 -0
- package/dist/validation/yaml.d.ts.map +1 -0
- package/dist/validation/yaml.js +30 -0
- package/dist/validation/yaml.js.map +1 -0
- package/dist/workflow/index.d.ts +10 -0
- package/dist/workflow/index.d.ts.map +1 -0
- package/dist/workflow/index.js +16 -0
- package/dist/workflow/index.js.map +1 -0
- package/dist/workflow/schema.d.ts +89 -0
- package/dist/workflow/schema.d.ts.map +1 -0
- package/dist/workflow/schema.js +73 -0
- package/dist/workflow/schema.js.map +1 -0
- package/package.json +78 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/** One commit's attribution, as audited against `pathspecs` (REQ-SEC-02's "0 unknown author"). */
|
|
2
|
+
export interface AttributionEntry {
|
|
3
|
+
readonly sha: string;
|
|
4
|
+
readonly authorName: string;
|
|
5
|
+
readonly authorEmail: string;
|
|
6
|
+
/** ISO-8601 author date (`git log --format=%aI`) — sourced from git, never file content. */
|
|
7
|
+
readonly date: string;
|
|
8
|
+
readonly subject: string;
|
|
9
|
+
/** Whether this commit's author identity is non-empty and not a git-guessed placeholder. */
|
|
10
|
+
readonly valid: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Whether `name`/`email` look like a real, deliberately-configured git identity rather than an
|
|
14
|
+
* empty or git-guessed placeholder, as recorded on a *historical* commit. The base check — non-empty
|
|
15
|
+
* `name` AND non-empty `email` — is NOT re-derived here: it delegates to
|
|
16
|
+
* {@link isConfiguredIdentity} from `src/core/git-identity.ts`, the single source of truth also used
|
|
17
|
+
* by `requireGitIdentity`'s write-time precondition (REQ-SEC-01). On top of that shared base, this
|
|
18
|
+
* audit (REQ-SEC-02) layers two read-only augmentations that only make sense when inspecting
|
|
19
|
+
* *historical* commits rather than live config — `requireGitIdentity` has no need for either, because
|
|
20
|
+
* it only ever looks at the identity a caller is about to write with:
|
|
21
|
+
*
|
|
22
|
+
* - Rejects the `GIT_GUESSED_DOMAIN_MARKER` (`.(none)`): git appends this to an author email when it
|
|
23
|
+
* fell back to a guessed identity at commit time. `requireGitIdentity` now PREVENTS this going
|
|
24
|
+
* forward (task-014), but older history predating that guard can still carry it, so the read audit
|
|
25
|
+
* must flag it — it is a legacy-history concern, not part of the live write-time rule.
|
|
26
|
+
* - Rejects a non-empty-but-malformed email (`EMAIL_RE`): `requireGitIdentity` never needs this check
|
|
27
|
+
* because it only reads a git config value (itself always syntactically well-formed or absent);
|
|
28
|
+
* historical commit authors, however, can carry hand-edited or otherwise malformed values, so the
|
|
29
|
+
* audit validates the shape too.
|
|
30
|
+
*
|
|
31
|
+
* Pure predicate — no filesystem/git access.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isValidAttribution(name: string, email: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Walk every commit touching any of `pathspecs` under `root`, oldest first, annotated with whether
|
|
36
|
+
* its author attribution is valid ({@link isValidAttribution}) — the audit primitive behind
|
|
37
|
+
* REQ-SEC-02's "0 'unknown author'" fit criterion. `pathspecs` is sorted before being passed to git
|
|
38
|
+
* so the invocation itself is deterministic regardless of caller-supplied order (REQ-SYS-07); a
|
|
39
|
+
* commit touching more than one of the given pathspecs still appears exactly once (git's own log
|
|
40
|
+
* de-duplicates by commit, not by path). Returns `[]` (never throws) when none of `pathspecs` has
|
|
41
|
+
* any history, mirroring {@link getMemoryHistory}.
|
|
42
|
+
*/
|
|
43
|
+
export declare function auditAttribution(root: string, pathspecs: readonly string[]): AttributionEntry[];
|
|
44
|
+
/** Parsed `Approver:`/`Reason:` lines from an `approve`/`reject` commit body (CLAUDE.md §5.1). */
|
|
45
|
+
export interface ApprovalMetadata {
|
|
46
|
+
readonly approverName: string;
|
|
47
|
+
readonly approverEmail: string;
|
|
48
|
+
readonly approverRole: string;
|
|
49
|
+
/**
|
|
50
|
+
* The `Reason:` block this commit records, or `null` when it records none that can be read
|
|
51
|
+
* (`dl-067-reason-trailer-contract` clause 6). Widened from `string` by
|
|
52
|
+
* `task-072-fix-reason-trailer-contract`: an unreadable reason must not take a successfully parsed
|
|
53
|
+
* approver identity down with it, and `MemoryTransition.reason` was already `string | null`.
|
|
54
|
+
*/
|
|
55
|
+
readonly reason: string | null;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The `Reason:` a commit body records, on its own — `null` when the body has none (a plain
|
|
59
|
+
* `add`/`submit` body, which carries no trailer by convention), or carries a bare `Reason:` with no
|
|
60
|
+
* value. Split out of {@link parseApprovalMetadata} by `task-049-memory-history` (P1.10) because the
|
|
61
|
+
* two trailers do not always travel together: an APPROVAL-gate commit (`approve`/`reject`) must carry
|
|
62
|
+
* both `Approver:` and `Reason:` (CLAUDE.md §5.1, P1.7), but a `deprecate` commit records a `Reason:`
|
|
63
|
+
* with **no** `Approver:` line at all — deprecate is explicitly not an approval gate. Reading the
|
|
64
|
+
* reason through `parseApprovalMetadata` would therefore drop every deprecate reason from the audit
|
|
65
|
+
* trail it exists to surface.
|
|
66
|
+
*
|
|
67
|
+
* The reason's extent is NOT decided here: this is a thin alias over `parseReasonBlock`
|
|
68
|
+
* (`./commit-message.ts`), the module that also writes the trailer — one grammar, consumed by both
|
|
69
|
+
* sides, which is `dl-067-reason-trailer-contract` clause 5 and the property that module's doc has
|
|
70
|
+
* claimed for itself since `task-045-memory-submit`.
|
|
71
|
+
*
|
|
72
|
+
* The limitation this function used to document — "a hypothetical multi-paragraph reason would be
|
|
73
|
+
* silently truncated to its first line here" — is gone with the defect
|
|
74
|
+
* (`bug-042-reason-text-has-no-contract-against-commit-trailer`, F1). It was never hypothetical: 79 of
|
|
75
|
+
* `main`'s 171 approve/reject commits carry a reason continuing past line one, and the reader kept as
|
|
76
|
+
* little as 2% of one (dl-067 E1/E2).
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseCommitReason(body: string): string | null;
|
|
79
|
+
/**
|
|
80
|
+
* Parse a commit body for the `Approver: Name <email> (role)` and `Reason: ...` trailers
|
|
81
|
+
* (CLAUDE.md §5.1; REQ-SEC-02/REQ-SEC-04). Returns `null` when there is no `Approver:` trailer line —
|
|
82
|
+
* e.g. for a plain `add`/`submit` body, or a `deprecate` one (not an approval gate), neither of which
|
|
83
|
+
* records an approval at all. A caller that wants such a commit's reason wants
|
|
84
|
+
* {@link parseCommitReason} instead.
|
|
85
|
+
*
|
|
86
|
+
* All-or-nothing on the APPROVER, and only on the approver (`dl-067` clause 6,
|
|
87
|
+
* `task-072-fix-reason-trailer-contract`). It used to be all-or-nothing in both directions, so an
|
|
88
|
+
* unreadable `Reason:` discarded the identity this function had already parsed successfully — and
|
|
89
|
+
* since an empty `--reason` was accepted at exit `0` and git's cleanup turned it into a bare
|
|
90
|
+
* `Reason:`, `wingfoil memory history` could report an approval gate crossed by nobody, for no reason
|
|
91
|
+
* (bug-042 F2). Now the record degrades instead of vanishing: the approver stands, `reason` reads
|
|
92
|
+
* `null`. Nothing on `main` reads differently for it — no commit there has a bare `Reason:` or is
|
|
93
|
+
* missing a trailer line (dl-067 E6) — so this is protection for bodies written before the fix.
|
|
94
|
+
*/
|
|
95
|
+
export declare function parseApprovalMetadata(body: string): ApprovalMetadata | null;
|
|
96
|
+
/** The CLAUDE.md §5.1 verb a commit's subject declares, when it matches the `wf({type}): {verb} ...` shape. */
|
|
97
|
+
type MemoryOperation = 'add' | 'submit' | 'approve' | 'reject' | 'deprecate';
|
|
98
|
+
/**
|
|
99
|
+
* One reconstructed transition in a Memory element's history — author/timestamp from git itself,
|
|
100
|
+
* `fromState`/`toState` derived from the document's own frontmatter `status:` field at that commit
|
|
101
|
+
* (ADR-007: state lives in frontmatter, not the commit message), and `approval` parsed from the
|
|
102
|
+
* commit body when present. `fromState` is `null` only for the element's first commit (creation —
|
|
103
|
+
* there is no prior state to name).
|
|
104
|
+
*/
|
|
105
|
+
export interface MemoryTransition {
|
|
106
|
+
readonly sha: string;
|
|
107
|
+
readonly authorName: string;
|
|
108
|
+
readonly authorEmail: string;
|
|
109
|
+
readonly date: string;
|
|
110
|
+
readonly subject: string;
|
|
111
|
+
readonly operation: MemoryOperation | null;
|
|
112
|
+
readonly fromState: string | null;
|
|
113
|
+
readonly toState: string | null;
|
|
114
|
+
readonly approval: ApprovalMetadata | null;
|
|
115
|
+
/**
|
|
116
|
+
* The `Reason:` this commit's body records, read INDEPENDENTLY of `Approver:`
|
|
117
|
+
* ({@link parseCommitReason}) — `null` when the body records none. Deliberately not the same field
|
|
118
|
+
* as `approval.reason`: `approval` is `null` for a `deprecate` commit (it has no `Approver:` line),
|
|
119
|
+
* yet such a commit does record a reason, and `wingfoil memory history` (P1.10) must surface it.
|
|
120
|
+
* When `approval` is non-null the two always agree, by construction.
|
|
121
|
+
*/
|
|
122
|
+
readonly reason: string | null;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Reconstruct every transition `relativePath` went through, oldest first, entirely from git log
|
|
126
|
+
* (`getMemoryHistory`) plus a frontmatter read at each commit (`git show`) — no separate log file
|
|
127
|
+
* (ADR-007). This is the derivation `wingfoil memory history` (P1.10) renders — `src/core`'s
|
|
128
|
+
* `memoryHistory` operation (`task-049-memory-history`) renames and string-formats these fields into
|
|
129
|
+
* its own entry shape, deriving no further state of its own; this function is the reconstruction
|
|
130
|
+
* itself, not that command's output formatting.
|
|
131
|
+
*
|
|
132
|
+
* **Renames are followed on both halves of the derivation**, which is the correction
|
|
133
|
+
* `task-097-memory-history-reads-each-commit-at-its-historical-path` made. `getMemoryHistory` always
|
|
134
|
+
* walked with `git log --follow`, so it *found* the commits preceding a rename; `readStatusAt` then
|
|
135
|
+
* read each of them at the CURRENT path, which those trees do not contain, so all of them reported
|
|
136
|
+
* `null` states (`bug-080-…`). The walk now carries the path each commit actually used
|
|
137
|
+
* (`MemoryHistoryEntry.path`, from `--follow --name-status`) and this function reads at that path —
|
|
138
|
+
* the walk is the source of truth for "where was this file at this commit", and the read no longer
|
|
139
|
+
* guesses.
|
|
140
|
+
*
|
|
141
|
+
* This was never the hypothetical the TSDoc here used to call "a documented edge case, not a live
|
|
142
|
+
* defect": `docs/self/docs/04_memory/planning/v1/*` became `planning/rl-v1/*` in a single commit,
|
|
143
|
+
* moving five `release` elements at once, precisely BECAUSE the `release` type's `path` pattern
|
|
144
|
+
* interpolates a component — the release-line id — that itself changed. Any edit to a `path`
|
|
145
|
+
* pattern, or to an id one interpolates, renames documents.
|
|
146
|
+
*
|
|
147
|
+
* `toState` is still `null` where a commit in the walk genuinely has no document to read — before
|
|
148
|
+
* the element's creation, or after a deletion. That is a legitimate answer, distinct from the defect
|
|
149
|
+
* above, and it no longer arrives with a `fatal:` on the operator's stderr (`bug-071`; see
|
|
150
|
+
* {@link readStatusAt}).
|
|
151
|
+
*/
|
|
152
|
+
export declare function reconstructMemoryTransitions(root: string, relativePath: string): MemoryTransition[];
|
|
153
|
+
/** One disagreement between the commit subject's `[old → new]` bracket and the frontmatter actually committed. */
|
|
154
|
+
export interface ConsistencyMismatch {
|
|
155
|
+
readonly sha: string;
|
|
156
|
+
readonly subject: string;
|
|
157
|
+
readonly declared: {
|
|
158
|
+
readonly from: string;
|
|
159
|
+
readonly to: string;
|
|
160
|
+
};
|
|
161
|
+
readonly derived: {
|
|
162
|
+
readonly from: string | null;
|
|
163
|
+
readonly to: string | null;
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Cross-check, for every transition that carries a `[old → new]` bracket in its subject (`approve`/
|
|
168
|
+
* `reject`/`deprecate`, per CLAUDE.md §5.1 — a plain `add`/`submit` subject has no bracket and is
|
|
169
|
+
* skipped), that the bracket's declared states agree with the states independently derived from the
|
|
170
|
+
* document's own frontmatter at that commit ({@link reconstructMemoryTransitions}). Returns `[]` when
|
|
171
|
+
* every bracketed transition agrees — REQ-SEC-02/REQ-STATE-02's "no drift between the two views";
|
|
172
|
+
* any entry in the returned array is a genuine inconsistency (e.g. a hand-edited commit message that
|
|
173
|
+
* doesn't match what was actually written to disk).
|
|
174
|
+
*/
|
|
175
|
+
export declare function verifyTransitionConsistency(root: string, relativePath: string): ConsistencyMismatch[];
|
|
176
|
+
export {};
|
|
177
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../src/memory/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isValidAttribution = isValidAttribution;
|
|
4
|
+
exports.auditAttribution = auditAttribution;
|
|
5
|
+
exports.parseCommitReason = parseCommitReason;
|
|
6
|
+
exports.parseApprovalMetadata = parseApprovalMetadata;
|
|
7
|
+
exports.reconstructMemoryTransitions = reconstructMemoryTransitions;
|
|
8
|
+
exports.verifyTransitionConsistency = verifyTransitionConsistency;
|
|
9
|
+
/**
|
|
10
|
+
* Audit-trail verification & reconstruction (task-015-complete-audit-trail, REQ-SEC-02) — the
|
|
11
|
+
* READ/verify half of "every state change is a git commit with author + timestamp" (ADR-001,
|
|
12
|
+
* ADR-007): git history is the *only* audit trail (no secondary `.wingfoil/state/` log), so this
|
|
13
|
+
* module audits and reconstructs that trail directly from `git log`/`git show`, never from a value
|
|
14
|
+
* written into file content. It builds on task-011's {@link getMemoryHistory} (the raw commit walk)
|
|
15
|
+
* rather than re-implementing it.
|
|
16
|
+
*
|
|
17
|
+
* This module deliberately does **not** enforce attribution at write time (checking that a commit
|
|
18
|
+
* *can* be made) — that precondition belongs to task-014-git-identity-required (REQ-SEC-01,
|
|
19
|
+
* `requireGitIdentity` in `src/core/git-identity.ts`). This module only ever reads commits that
|
|
20
|
+
* already exist and reports on them; nothing here blocks or gates a write. The base "non-empty
|
|
21
|
+
* name and non-empty email" rule the two share lives in exactly one place — the
|
|
22
|
+
* `isConfiguredIdentity` predicate exported by `src/core/git-identity.ts` — which
|
|
23
|
+
* {@link isValidAttribution} below consumes rather than re-deriving; see that function's
|
|
24
|
+
* doc-comment for the read-only augmentations it layers on top for historical commits.
|
|
25
|
+
*
|
|
26
|
+
* REQ-SEC-02's fit criterion, mapped to the functions below:
|
|
27
|
+
*
|
|
28
|
+
* - "`git log` verification ... shows author + timestamp for every change with 0 'unknown
|
|
29
|
+
* author'" -> {@link auditAttribution} / {@link isValidAttribution}.
|
|
30
|
+
* - Commit timestamp (never file content) supplies the ISO-8601 date -> already true of
|
|
31
|
+
* `getMemoryHistory`'s `date` field (`%aI`, sourced from git itself); this module never re-derives
|
|
32
|
+
* a timestamp from anywhere else.
|
|
33
|
+
* - "`approve`/`reject` commits carry an explicit `Approver:`/`Reason:` line" ->
|
|
34
|
+
* {@link parseApprovalMetadata}.
|
|
35
|
+
* - "`memory history <id>` lists each transition ... author, timestamp, and reason" ->
|
|
36
|
+
* {@link reconstructMemoryTransitions}.
|
|
37
|
+
* - "Recomputing an element's state at any historical commit ... agrees with the transition history
|
|
38
|
+
* derived from git log ... no drift" -> {@link verifyTransitionConsistency}.
|
|
39
|
+
*/
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const core_1 = require("../core");
|
|
42
|
+
const validation_1 = require("../validation");
|
|
43
|
+
const commit_message_1 = require("./commit-message");
|
|
44
|
+
const history_1 = require("./history");
|
|
45
|
+
const git_log_1 = require("./git-log");
|
|
46
|
+
const storage_1 = require("../storage");
|
|
47
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@()]+$/;
|
|
48
|
+
// The literal marker git itself appends to an author email when it falls back to a guessed identity
|
|
49
|
+
// (user@hostname) and cannot determine a real domain — e.g. "root@buildhost.(none)". A commit
|
|
50
|
+
// carrying this is not a deliberately-configured identity, so it counts as "unknown author".
|
|
51
|
+
const GIT_GUESSED_DOMAIN_MARKER = '.(none)';
|
|
52
|
+
/**
|
|
53
|
+
* Whether `name`/`email` look like a real, deliberately-configured git identity rather than an
|
|
54
|
+
* empty or git-guessed placeholder, as recorded on a *historical* commit. The base check — non-empty
|
|
55
|
+
* `name` AND non-empty `email` — is NOT re-derived here: it delegates to
|
|
56
|
+
* {@link isConfiguredIdentity} from `src/core/git-identity.ts`, the single source of truth also used
|
|
57
|
+
* by `requireGitIdentity`'s write-time precondition (REQ-SEC-01). On top of that shared base, this
|
|
58
|
+
* audit (REQ-SEC-02) layers two read-only augmentations that only make sense when inspecting
|
|
59
|
+
* *historical* commits rather than live config — `requireGitIdentity` has no need for either, because
|
|
60
|
+
* it only ever looks at the identity a caller is about to write with:
|
|
61
|
+
*
|
|
62
|
+
* - Rejects the `GIT_GUESSED_DOMAIN_MARKER` (`.(none)`): git appends this to an author email when it
|
|
63
|
+
* fell back to a guessed identity at commit time. `requireGitIdentity` now PREVENTS this going
|
|
64
|
+
* forward (task-014), but older history predating that guard can still carry it, so the read audit
|
|
65
|
+
* must flag it — it is a legacy-history concern, not part of the live write-time rule.
|
|
66
|
+
* - Rejects a non-empty-but-malformed email (`EMAIL_RE`): `requireGitIdentity` never needs this check
|
|
67
|
+
* because it only reads a git config value (itself always syntactically well-formed or absent);
|
|
68
|
+
* historical commit authors, however, can carry hand-edited or otherwise malformed values, so the
|
|
69
|
+
* audit validates the shape too.
|
|
70
|
+
*
|
|
71
|
+
* Pure predicate — no filesystem/git access.
|
|
72
|
+
*/
|
|
73
|
+
function isValidAttribution(name, email) {
|
|
74
|
+
const trimmedName = name.trim();
|
|
75
|
+
const trimmedEmail = email.trim();
|
|
76
|
+
if (!(0, core_1.isConfiguredIdentity)(trimmedName, trimmedEmail))
|
|
77
|
+
return false;
|
|
78
|
+
if (trimmedEmail.includes(GIT_GUESSED_DOMAIN_MARKER))
|
|
79
|
+
return false;
|
|
80
|
+
return EMAIL_RE.test(trimmedEmail);
|
|
81
|
+
}
|
|
82
|
+
const AUDIT_LOG_FIELDS = ['%H', '%an', '%ae', '%aI', '%s'];
|
|
83
|
+
/**
|
|
84
|
+
* Walk every commit touching any of `pathspecs` under `root`, oldest first, annotated with whether
|
|
85
|
+
* its author attribution is valid ({@link isValidAttribution}) — the audit primitive behind
|
|
86
|
+
* REQ-SEC-02's "0 'unknown author'" fit criterion. `pathspecs` is sorted before being passed to git
|
|
87
|
+
* so the invocation itself is deterministic regardless of caller-supplied order (REQ-SYS-07); a
|
|
88
|
+
* commit touching more than one of the given pathspecs still appears exactly once (git's own log
|
|
89
|
+
* de-duplicates by commit, not by path). Returns `[]` (never throws) when none of `pathspecs` has
|
|
90
|
+
* any history, mirroring {@link getMemoryHistory}.
|
|
91
|
+
*/
|
|
92
|
+
function auditAttribution(root, pathspecs) {
|
|
93
|
+
const sortedPathspecs = [...pathspecs].sort();
|
|
94
|
+
const records = (0, git_log_1.walkGitLogFields)(root, AUDIT_LOG_FIELDS, sortedPathspecs);
|
|
95
|
+
return records.map(([sha = '', authorName = '', authorEmail = '', date = '', subject = '']) => ({
|
|
96
|
+
sha,
|
|
97
|
+
authorName,
|
|
98
|
+
authorEmail,
|
|
99
|
+
date,
|
|
100
|
+
subject,
|
|
101
|
+
valid: isValidAttribution(authorName, authorEmail),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The identity on an `Approver:` trailer line. Applied to that ONE line — located by
|
|
106
|
+
* {@link parseApproverTrailerLine}, which anchors it to the body's first — rather than searched for
|
|
107
|
+
* anywhere in the body with `/m`, so a second `Approver:` line sitting inside some reason's text can
|
|
108
|
+
* never be read as an approval record (`dl-067` clause 5; bug-042 F3).
|
|
109
|
+
*/
|
|
110
|
+
const APPROVER_LINE_RE = /^Approver:\s*(.+?)\s*<([^>]+)>\s*\(([^)]+)\)\s*$/;
|
|
111
|
+
/**
|
|
112
|
+
* The `Reason:` a commit body records, on its own — `null` when the body has none (a plain
|
|
113
|
+
* `add`/`submit` body, which carries no trailer by convention), or carries a bare `Reason:` with no
|
|
114
|
+
* value. Split out of {@link parseApprovalMetadata} by `task-049-memory-history` (P1.10) because the
|
|
115
|
+
* two trailers do not always travel together: an APPROVAL-gate commit (`approve`/`reject`) must carry
|
|
116
|
+
* both `Approver:` and `Reason:` (CLAUDE.md §5.1, P1.7), but a `deprecate` commit records a `Reason:`
|
|
117
|
+
* with **no** `Approver:` line at all — deprecate is explicitly not an approval gate. Reading the
|
|
118
|
+
* reason through `parseApprovalMetadata` would therefore drop every deprecate reason from the audit
|
|
119
|
+
* trail it exists to surface.
|
|
120
|
+
*
|
|
121
|
+
* The reason's extent is NOT decided here: this is a thin alias over `parseReasonBlock`
|
|
122
|
+
* (`./commit-message.ts`), the module that also writes the trailer — one grammar, consumed by both
|
|
123
|
+
* sides, which is `dl-067-reason-trailer-contract` clause 5 and the property that module's doc has
|
|
124
|
+
* claimed for itself since `task-045-memory-submit`.
|
|
125
|
+
*
|
|
126
|
+
* The limitation this function used to document — "a hypothetical multi-paragraph reason would be
|
|
127
|
+
* silently truncated to its first line here" — is gone with the defect
|
|
128
|
+
* (`bug-042-reason-text-has-no-contract-against-commit-trailer`, F1). It was never hypothetical: 79 of
|
|
129
|
+
* `main`'s 171 approve/reject commits carry a reason continuing past line one, and the reader kept as
|
|
130
|
+
* little as 2% of one (dl-067 E1/E2).
|
|
131
|
+
*/
|
|
132
|
+
function parseCommitReason(body) {
|
|
133
|
+
return (0, commit_message_1.parseReasonBlock)(body);
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Parse a commit body for the `Approver: Name <email> (role)` and `Reason: ...` trailers
|
|
137
|
+
* (CLAUDE.md §5.1; REQ-SEC-02/REQ-SEC-04). Returns `null` when there is no `Approver:` trailer line —
|
|
138
|
+
* e.g. for a plain `add`/`submit` body, or a `deprecate` one (not an approval gate), neither of which
|
|
139
|
+
* records an approval at all. A caller that wants such a commit's reason wants
|
|
140
|
+
* {@link parseCommitReason} instead.
|
|
141
|
+
*
|
|
142
|
+
* All-or-nothing on the APPROVER, and only on the approver (`dl-067` clause 6,
|
|
143
|
+
* `task-072-fix-reason-trailer-contract`). It used to be all-or-nothing in both directions, so an
|
|
144
|
+
* unreadable `Reason:` discarded the identity this function had already parsed successfully — and
|
|
145
|
+
* since an empty `--reason` was accepted at exit `0` and git's cleanup turned it into a bare
|
|
146
|
+
* `Reason:`, `wingfoil memory history` could report an approval gate crossed by nobody, for no reason
|
|
147
|
+
* (bug-042 F2). Now the record degrades instead of vanishing: the approver stands, `reason` reads
|
|
148
|
+
* `null`. Nothing on `main` reads differently for it — no commit there has a bare `Reason:` or is
|
|
149
|
+
* missing a trailer line (dl-067 E6) — so this is protection for bodies written before the fix.
|
|
150
|
+
*/
|
|
151
|
+
function parseApprovalMetadata(body) {
|
|
152
|
+
const approverLine = (0, commit_message_1.parseApproverTrailerLine)(body);
|
|
153
|
+
const approverMatch = approverLine === null ? null : APPROVER_LINE_RE.exec(approverLine);
|
|
154
|
+
if (!approverMatch)
|
|
155
|
+
return null;
|
|
156
|
+
const [, approverName = '', approverEmail = '', approverRole = ''] = approverMatch;
|
|
157
|
+
return { approverName, approverEmail, approverRole, reason: parseCommitReason(body) };
|
|
158
|
+
}
|
|
159
|
+
const OPERATION_RE = /^wf\([^)]+\):\s*(add|submit|approve|reject|deprecate)\b/;
|
|
160
|
+
/**
|
|
161
|
+
* Read `historicalPath`'s frontmatter `status:` field as it existed at `sha` (`git show sha:path`),
|
|
162
|
+
* without validating it against any type's Zod schema — this is a historical-snapshot read, not a
|
|
163
|
+
* live document validation. Returns `null` if the path didn't exist at `sha`, has no frontmatter
|
|
164
|
+
* block, or the frontmatter has no string `status` field.
|
|
165
|
+
*
|
|
166
|
+
* `historicalPath` is the path the element occupied **at that commit** — `MemoryHistoryEntry.path`,
|
|
167
|
+
* produced by the same `--follow` walk that selected the commit — never the caller's current path.
|
|
168
|
+
* Passing the current one was `bug-080-read-status-at-reads-the-current-path-at-pre-rename-commits`:
|
|
169
|
+
* every commit older than a rename asked git for a path that tree does not contain, the read failed,
|
|
170
|
+
* and the transition reported `null` states. `dl-080` — a read that gates an operation resolves at
|
|
171
|
+
* `HEAD` — does not reach here: this read gates nothing, and its subject is by construction *other*
|
|
172
|
+
* commits.
|
|
173
|
+
*
|
|
174
|
+
* The explicit `stdio` is `bug-071-read-status-at-leaks-git-stderr`, and it is not made redundant by
|
|
175
|
+
* the paragraph above. `execFileSync` hands the child this process's own stderr by default, so on
|
|
176
|
+
* the failure that remains legitimate — a commit where the document genuinely does not exist, before
|
|
177
|
+
* its creation or after a deletion — git's `fatal: path ... does not exist in '<sha>'` went straight
|
|
178
|
+
* to the operator's terminal while the command reported success. Capturing it into a pipe this
|
|
179
|
+
* process owns (and discarding it, the failure being handled right here) is what keeps a genuine
|
|
180
|
+
* `fatal:` distinguishable from a routine one — that line is how `bug-050`'s forged history
|
|
181
|
+
* announced itself.
|
|
182
|
+
*/
|
|
183
|
+
function readStatusAt(root, sha, historicalPath) {
|
|
184
|
+
let raw;
|
|
185
|
+
try {
|
|
186
|
+
raw = (0, child_process_1.execFileSync)('git', ['-C', root, 'show', `${sha}:${historicalPath}`], {
|
|
187
|
+
encoding: 'utf-8',
|
|
188
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const { frontmatter } = (0, storage_1.splitFrontmatter)(raw);
|
|
195
|
+
if (!frontmatter)
|
|
196
|
+
return null;
|
|
197
|
+
const parsed = (0, validation_1.parseYaml)(frontmatter, `${historicalPath}@${sha}`);
|
|
198
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
199
|
+
return null;
|
|
200
|
+
const status = parsed.status;
|
|
201
|
+
return typeof status === 'string' ? status : null;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Reconstruct every transition `relativePath` went through, oldest first, entirely from git log
|
|
205
|
+
* (`getMemoryHistory`) plus a frontmatter read at each commit (`git show`) — no separate log file
|
|
206
|
+
* (ADR-007). This is the derivation `wingfoil memory history` (P1.10) renders — `src/core`'s
|
|
207
|
+
* `memoryHistory` operation (`task-049-memory-history`) renames and string-formats these fields into
|
|
208
|
+
* its own entry shape, deriving no further state of its own; this function is the reconstruction
|
|
209
|
+
* itself, not that command's output formatting.
|
|
210
|
+
*
|
|
211
|
+
* **Renames are followed on both halves of the derivation**, which is the correction
|
|
212
|
+
* `task-097-memory-history-reads-each-commit-at-its-historical-path` made. `getMemoryHistory` always
|
|
213
|
+
* walked with `git log --follow`, so it *found* the commits preceding a rename; `readStatusAt` then
|
|
214
|
+
* read each of them at the CURRENT path, which those trees do not contain, so all of them reported
|
|
215
|
+
* `null` states (`bug-080-…`). The walk now carries the path each commit actually used
|
|
216
|
+
* (`MemoryHistoryEntry.path`, from `--follow --name-status`) and this function reads at that path —
|
|
217
|
+
* the walk is the source of truth for "where was this file at this commit", and the read no longer
|
|
218
|
+
* guesses.
|
|
219
|
+
*
|
|
220
|
+
* This was never the hypothetical the TSDoc here used to call "a documented edge case, not a live
|
|
221
|
+
* defect": `docs/self/docs/04_memory/planning/v1/*` became `planning/rl-v1/*` in a single commit,
|
|
222
|
+
* moving five `release` elements at once, precisely BECAUSE the `release` type's `path` pattern
|
|
223
|
+
* interpolates a component — the release-line id — that itself changed. Any edit to a `path`
|
|
224
|
+
* pattern, or to an id one interpolates, renames documents.
|
|
225
|
+
*
|
|
226
|
+
* `toState` is still `null` where a commit in the walk genuinely has no document to read — before
|
|
227
|
+
* the element's creation, or after a deletion. That is a legitimate answer, distinct from the defect
|
|
228
|
+
* above, and it no longer arrives with a `fatal:` on the operator's stderr (`bug-071`; see
|
|
229
|
+
* {@link readStatusAt}).
|
|
230
|
+
*/
|
|
231
|
+
function reconstructMemoryTransitions(root, relativePath) {
|
|
232
|
+
const history = (0, history_1.getMemoryHistory)(root, relativePath); // oldest first already
|
|
233
|
+
const transitions = [];
|
|
234
|
+
let previousState = null;
|
|
235
|
+
for (const entry of history) {
|
|
236
|
+
const toState = readStatusAt(root, entry.sha, entry.path);
|
|
237
|
+
const operationMatch = OPERATION_RE.exec(entry.subject);
|
|
238
|
+
transitions.push({
|
|
239
|
+
sha: entry.sha,
|
|
240
|
+
authorName: entry.authorName,
|
|
241
|
+
authorEmail: entry.authorEmail,
|
|
242
|
+
date: entry.date,
|
|
243
|
+
subject: entry.subject,
|
|
244
|
+
operation: operationMatch?.[1] ?? null,
|
|
245
|
+
fromState: previousState,
|
|
246
|
+
toState,
|
|
247
|
+
approval: parseApprovalMetadata(entry.body),
|
|
248
|
+
reason: parseCommitReason(entry.body),
|
|
249
|
+
});
|
|
250
|
+
previousState = toState;
|
|
251
|
+
}
|
|
252
|
+
return transitions;
|
|
253
|
+
}
|
|
254
|
+
// Matches the `[old-state → new-state]` bracket CLAUDE.md §5.1 mandates on approve/reject/deprecate
|
|
255
|
+
// subjects (the arrow is the U+2192 character used throughout CLAUDE.md/the memory templates).
|
|
256
|
+
const BRACKET_RE = /\[([^[\]→]+?)\s*→\s*([^[\]]+?)\]\s*$/;
|
|
257
|
+
/**
|
|
258
|
+
* Cross-check, for every transition that carries a `[old → new]` bracket in its subject (`approve`/
|
|
259
|
+
* `reject`/`deprecate`, per CLAUDE.md §5.1 — a plain `add`/`submit` subject has no bracket and is
|
|
260
|
+
* skipped), that the bracket's declared states agree with the states independently derived from the
|
|
261
|
+
* document's own frontmatter at that commit ({@link reconstructMemoryTransitions}). Returns `[]` when
|
|
262
|
+
* every bracketed transition agrees — REQ-SEC-02/REQ-STATE-02's "no drift between the two views";
|
|
263
|
+
* any entry in the returned array is a genuine inconsistency (e.g. a hand-edited commit message that
|
|
264
|
+
* doesn't match what was actually written to disk).
|
|
265
|
+
*/
|
|
266
|
+
function verifyTransitionConsistency(root, relativePath) {
|
|
267
|
+
const transitions = reconstructMemoryTransitions(root, relativePath);
|
|
268
|
+
const mismatches = [];
|
|
269
|
+
for (const transition of transitions) {
|
|
270
|
+
const match = BRACKET_RE.exec(transition.subject);
|
|
271
|
+
if (!match)
|
|
272
|
+
continue;
|
|
273
|
+
const [, declaredFromRaw = '', declaredToRaw = ''] = match;
|
|
274
|
+
const declaredFrom = declaredFromRaw.trim();
|
|
275
|
+
const declaredTo = declaredToRaw.trim();
|
|
276
|
+
if (declaredFrom !== transition.fromState || declaredTo !== transition.toState) {
|
|
277
|
+
mismatches.push({
|
|
278
|
+
sha: transition.sha,
|
|
279
|
+
subject: transition.subject,
|
|
280
|
+
declared: { from: declaredFrom, to: declaredTo },
|
|
281
|
+
derived: { from: transition.fromState, to: transition.toState },
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return mismatches;
|
|
286
|
+
}
|
|
287
|
+
//# sourceMappingURL=audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.js","sourceRoot":"","sources":["../../src/memory/audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one commit-message formatter for Memory state-transition verbs (task-045-memory-submit).
|
|
3
|
+
*
|
|
4
|
+
* Every verb — `submit` today, `approve`/`reject`/`deprecate` next — produces exactly one commit whose
|
|
5
|
+
* subject is `wf({type}): {verb} {id1}, {id2}` (`spec-004-mcp-surface-contract` §4.3), optionally
|
|
6
|
+
* followed by a ` [from → to]` bracket and an `Approver:` / `Reason:` body for the verbs whose
|
|
7
|
+
* evidence rules require them (P1.7/P1.8, REQ-SEC-04). Those are exactly the shapes
|
|
8
|
+
* `./audit.ts` reads back for `wingfoil memory history` (P1.10): `OPERATION_RE` on the subject,
|
|
9
|
+
* `BRACKET_RE` on the bracket, `parseApprovalMetadata`/`parseCommitReason` on the body. Building the
|
|
10
|
+
* message in one place is what keeps writer and reader from drifting apart.
|
|
11
|
+
*
|
|
12
|
+
* Since `task-072-fix-reason-trailer-contract` this module also owns the **reason grammar** —
|
|
13
|
+
* {@link normalizeReason}, {@link reasonDefect}, {@link parseReasonBlock} and
|
|
14
|
+
* {@link parseApproverTrailerLine} — ratified by `dl-067-reason-trailer-contract` and fixing
|
|
15
|
+
* `bug-042-reason-text-has-no-contract-against-commit-trailer`. `--reason <text>` is arbitrary free
|
|
16
|
+
* text and the trailer is line-oriented; before dl-067 no rule said what happened when the two met, so
|
|
17
|
+
* a blank reason was recorded as a bare `Reason:` that destroyed the whole approval record, a
|
|
18
|
+
* multi-line one was truncated to its first line on read, and one shaped like a trailer could forge a
|
|
19
|
+
* second `Approver:` line. Putting the grammar HERE, next to the formatter, and having `./audit.ts`
|
|
20
|
+
* consume it is what makes this module doc's "one place" claim true rather than aspirational: the
|
|
21
|
+
* writer cannot emit a reason the reader cannot recover.
|
|
22
|
+
*
|
|
23
|
+
* Pure and deterministic (REQ-SYS-07): the output is a function of the input alone; ids keep the order
|
|
24
|
+
* the caller gives.
|
|
25
|
+
*/
|
|
26
|
+
import type { TransitionOp } from './state-machine';
|
|
27
|
+
/** Why a `--reason` value cannot be recorded in the trailer, per `dl-067` clause 4 and its corollary. */
|
|
28
|
+
export type ReasonDefect =
|
|
29
|
+
/** Empty, or nothing but whitespace: git would store a bare `Reason:` and both parsers would fail. */
|
|
30
|
+
'blank'
|
|
31
|
+
/** Carries a line starting `Approver:` or `Reason:` — a forged trailer line (bug-042 F3). */
|
|
32
|
+
| 'reserved-trailer-line'
|
|
33
|
+
/**
|
|
34
|
+
* Ends in a paragraph made entirely of trailer-shaped lines, which {@link parseReasonBlock} would
|
|
35
|
+
* read as git's own trailer block and drop. The corollary of the block's termination rule: without
|
|
36
|
+
* it, the round-trip equality dl-067 clause 3 promises would be false for this one input shape. 0 of
|
|
37
|
+
* `main`'s 172 reason blocks end this way, so the refusal costs the existing corpus nothing.
|
|
38
|
+
*/
|
|
39
|
+
| 'trailing-trailer-paragraph';
|
|
40
|
+
/**
|
|
41
|
+
* The refusal message for `defect` — the single source of the wording the CLI prints and
|
|
42
|
+
* `spec-008-cli-grammar` §2 pins. Deliberately distinct from `missing required argument: --reason`
|
|
43
|
+
* (`src/core/require-reason.ts`), which answers the *omitted* case that spec-008 §2 and the P1.7/P1.8
|
|
44
|
+
* BDD features already quote verbatim (dl-067 S1).
|
|
45
|
+
*/
|
|
46
|
+
export declare function reasonDefectMessage(defect: ReasonDefect): string;
|
|
47
|
+
/**
|
|
48
|
+
* The declared normal form of a reason (`dl-067` clause 3) — two rules, from two different sources,
|
|
49
|
+
* and it is worth keeping them apart:
|
|
50
|
+
*
|
|
51
|
+
* 1. **git's**, which applies whether or not this function exists. `commitPaths`
|
|
52
|
+
* (`src/storage/commit.ts`) commits with `-m`, so git's `cleanup=whitespace` strips per-line
|
|
53
|
+
* trailing whitespace, collapses runs of blank lines to one, and drops leading and trailing blank
|
|
54
|
+
* lines. This is why `spec-008-cli-grammar` §2's original "Recorded verbatim" was already false
|
|
55
|
+
* for any multi-line text, independently of bug-042.
|
|
56
|
+
* 2. **This module's**: the first line's leading whitespace is trimmed. git does NOT do this. It is
|
|
57
|
+
* needed because that line sits after `Reason: ` on the same physical line and
|
|
58
|
+
* {@link parseReasonBlock} consumes the key together with the whitespace that follows it — so
|
|
59
|
+
* without the trim, a reason beginning with spaces would not round-trip equally.
|
|
60
|
+
*
|
|
61
|
+
* Applying both HERE, on the way in, is what turns "approximately what you typed" into an assertable
|
|
62
|
+
* equality: what {@link parseReasonBlock} reads back out of the commit is exactly this function's
|
|
63
|
+
* output. Interior indentation is preserved by both rules — it carries meaning in real approval prose.
|
|
64
|
+
*
|
|
65
|
+
* Idempotent, and a pure function of its input (REQ-SYS-07).
|
|
66
|
+
*/
|
|
67
|
+
export declare function normalizeReason(reason: string): string;
|
|
68
|
+
/**
|
|
69
|
+
* Why `reason` cannot be recorded in the `Reason:` trailer, or `null` when it can (`dl-067` clause 4).
|
|
70
|
+
*
|
|
71
|
+
* Judged on the NORMALIZED text ({@link normalizeReason}), because that is what would actually be
|
|
72
|
+
* stored — otherwise a defect could hide behind trailing whitespace git strips on the way in. This is
|
|
73
|
+
* the ONE place the rule is decided: `src/core/require-reason.ts` turns a non-`null` result into the
|
|
74
|
+
* `UsageError` (exit 2) the CLI boundary reports, and {@link formatMemoryCommitMessage} refuses the
|
|
75
|
+
* same values so no caller can route around that boundary.
|
|
76
|
+
*/
|
|
77
|
+
export declare function reasonDefect(reason: string): ReasonDefect | null;
|
|
78
|
+
/**
|
|
79
|
+
* The `Reason:` a commit body records, read as a BLOCK (`dl-067` clause 2): the remainder of the
|
|
80
|
+
* `Reason:` line plus every following body line, up to (exclusive) git's trailing trailer paragraph or
|
|
81
|
+
* the end of the body. `null` when the body records no reason, or records a bare `Reason:` with no
|
|
82
|
+
* value — the shape git's cleanup leaves behind when an empty reason is written (bug-042 F2), which
|
|
83
|
+
* `parseApprovalMetadata` degrades around rather than treating as an absent approval.
|
|
84
|
+
*
|
|
85
|
+
* This replaces the first-line capture `/^Reason:\s*(.+)$/m` that preceded it, and that is a real
|
|
86
|
+
* change to how existing history reads: 79 of `main`'s 171 approve/reject commits (46%) carry a reason
|
|
87
|
+
* continuing past line one, and every one of them was silently halved. It is a recovery, not a
|
|
88
|
+
* reinterpretation — the text was always in the commit; only the reader dropped it.
|
|
89
|
+
*
|
|
90
|
+
* A generic `Key: value` line INSIDE the block is ordinary prose and does not end it; only a trailing
|
|
91
|
+
* paragraph made entirely of trailer lines does (dl-067 E5).
|
|
92
|
+
*/
|
|
93
|
+
export declare function parseReasonBlock(body: string): string | null;
|
|
94
|
+
/**
|
|
95
|
+
* The commit body's `Approver:` trailer line, or `null` when it has none — anchored to the body's
|
|
96
|
+
* FIRST non-empty line (`dl-067` clause 5). {@link formatMemoryCommitMessage} has always emitted it
|
|
97
|
+
* there, and it is the first body line in 171 of `main`'s 171 approve/reject commits (dl-067 E4), so
|
|
98
|
+
* anchoring costs no existing commit its approver while closing bug-042 F3 on the read side: an
|
|
99
|
+
* `Approver:` line appearing anywhere else is text some reason happens to contain, never an approval
|
|
100
|
+
* record. `./audit.ts` destructures the returned line into an identity — that shape is its concern;
|
|
101
|
+
* the line's POSITION is this module's.
|
|
102
|
+
*/
|
|
103
|
+
export declare function parseApproverTrailerLine(body: string): string | null;
|
|
104
|
+
/** The identity recorded on an approval-gate commit's `Approver:` line. */
|
|
105
|
+
export interface CommitApprover {
|
|
106
|
+
readonly name: string;
|
|
107
|
+
readonly email: string;
|
|
108
|
+
readonly role: string;
|
|
109
|
+
}
|
|
110
|
+
/** Everything one Memory transition commit message is built from. */
|
|
111
|
+
export interface MemoryCommitMessageInput {
|
|
112
|
+
/** The Memory element type all `ids` belong to — one commit is scoped to one type. */
|
|
113
|
+
readonly type: string;
|
|
114
|
+
/** The verb, which is also the subject's operation word. `add` is included for completeness. */
|
|
115
|
+
readonly op: TransitionOp | 'add';
|
|
116
|
+
/** The element ids moved by this commit, in the order they are listed in the subject. */
|
|
117
|
+
readonly ids: readonly string[];
|
|
118
|
+
/** When given, appended to the subject as ` [from → to]`. Omitted for `add`/`submit` (spec-004 §4.3). */
|
|
119
|
+
readonly transition?: {
|
|
120
|
+
readonly from: string;
|
|
121
|
+
readonly to: string;
|
|
122
|
+
};
|
|
123
|
+
/** When given, an `Approver: Name <email> (role)` body line. */
|
|
124
|
+
readonly approver?: CommitApprover;
|
|
125
|
+
/**
|
|
126
|
+
* When given, a `Reason:` block ({@link parseReasonBlock}) — written in the declared normal form
|
|
127
|
+
* ({@link normalizeReason}), and refused outright when {@link reasonDefect} says it could not be
|
|
128
|
+
* read back.
|
|
129
|
+
*/
|
|
130
|
+
readonly reason?: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Format a Memory transition commit message: the subject, then — only when an approver or reason is
|
|
134
|
+
* given — a blank line and the `Approver:` / `Reason:` lines in that order.
|
|
135
|
+
*
|
|
136
|
+
* The reason is emitted in its declared normal form and, when it carries a defect
|
|
137
|
+
* ({@link reasonDefect}), refused rather than written. The CLI boundary
|
|
138
|
+
* (`src/core/require-reason.ts`) already refuses the same values with the same messages at exit 2, so
|
|
139
|
+
* reaching this throw means a caller bypassed that boundary — it exists so no *future* caller can
|
|
140
|
+
* (dl-067 clause 5; bug-042 F2/F3).
|
|
141
|
+
*
|
|
142
|
+
* @throws `Error` when `ids` is empty: a transition commit always names what it moved.
|
|
143
|
+
* @throws `Error` when `reason` cannot be recorded in the trailer, carrying that defect's message.
|
|
144
|
+
*/
|
|
145
|
+
export declare function formatMemoryCommitMessage(input: MemoryCommitMessageInput): string;
|
|
146
|
+
//# sourceMappingURL=commit-message.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"commit-message.d.ts","sourceRoot":"","sources":["../../src/memory/commit-message.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AA6BpD,yGAAyG;AACzG,MAAM,MAAM,YAAY;AACtB,sGAAsG;AACpG,OAAO;AACT,6FAA6F;GAC3F,uBAAuB;AACzB;;;;;GAKG;GACD,4BAA4B,CAAC;AAUjC;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAEhE;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAUtD;AAYD;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,CAahE;AAsBD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAU5D;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAMpE;AAED,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACvC,sFAAsF;IACtF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,gGAAgG;IAChG,QAAQ,CAAC,EAAE,EAAE,YAAY,GAAG,KAAK,CAAC;IAClC,yFAAyF;IACzF,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,yGAAyG;IACzG,QAAQ,CAAC,UAAU,CAAC,EAAE;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;IACnC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,wBAAwB,GAAG,MAAM,CAmBjF"}
|