bmad-module-skill-forge 5.0.0 → 6.0.0
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/.claude-plugin/marketplace.json +1 -1
- package/docs/_data/pinned.yaml +1 -1
- package/docs/agents.md +1 -1
- package/docs/architecture.md +3 -3
- package/docs/campaign.md +1 -1
- package/docs/troubleshooting.md +9 -3
- package/docs/verifying-a-skill.md +9 -9
- package/docs/workflows.md +4 -4
- package/package.json +1 -1
- package/src/knowledge/tool-resolution.md +6 -6
- package/src/shared/references/headless-gate-convention.md +4 -1
- package/src/shared/references/output-contract-schema.md +3 -3
- package/src/shared/references/pipeline-contracts.md +2 -1
- package/src/shared/scripts/schemas/skf-test-result-envelope.v1.json +1 -1
- package/src/shared/scripts/skf-build-change-manifest.py +332 -54
- package/src/shared/scripts/skf-classify-changed-files.py +15 -10
- package/src/shared/scripts/skf-emit-result-envelope.py +58 -15
- package/src/shared/scripts/skf-extract-public-api.py +33 -13
- package/src/shared/scripts/skf-parse-gaps.py +3 -2
- package/src/shared/scripts/skf-verify-provenance-completeness.py +108 -11
- package/src/skf-analyze-source/SKILL.md +1 -0
- package/src/skf-analyze-source/references/generate-briefs.md +1 -1
- package/src/skf-analyze-source/references/headless-contract.md +1 -1
- package/src/skf-audit-skill/SKILL.md +2 -1
- package/src/skf-audit-skill/references/report.md +1 -1
- package/src/skf-brief-skill/SKILL.md +1 -0
- package/src/skf-brief-skill/references/invocation-contract.md +2 -2
- package/src/skf-campaign/SKILL.md +1 -0
- package/src/skf-campaign/references/campaign-contracts.md +2 -0
- package/src/skf-campaign/references/step-05-skill-loop.md +2 -2
- package/src/skf-campaign/references/step-10-export.md +3 -2
- package/src/skf-create-skill/SKILL.md +2 -1
- package/src/skf-create-skill/references/ccc-index-check.md +1 -1
- package/src/skf-create-skill/references/component-extraction.md +1 -1
- package/src/skf-create-skill/references/extract.md +1 -1
- package/src/skf-create-skill/references/generate-artifacts.md +1 -1
- package/src/skf-create-skill/references/report.md +2 -2
- package/src/skf-create-skill/references/sub/ccc-discover.md +1 -1
- package/src/skf-create-skill/references/validate.md +1 -1
- package/src/skf-create-stack-skill/SKILL.md +1 -0
- package/src/skf-create-stack-skill/references/generate-output.md +1 -1
- package/src/skf-create-stack-skill/references/invocation-contract.md +1 -1
- package/src/skf-create-stack-skill/references/parallel-extract.md +2 -2
- package/src/skf-create-stack-skill/references/report.md +1 -1
- package/src/skf-drop-skill/SKILL.md +1 -0
- package/src/skf-drop-skill/references/invocation-contract.md +1 -1
- package/src/skf-drop-skill/references/report.md +1 -1
- package/src/skf-export-skill/SKILL.md +1 -0
- package/src/skf-export-skill/references/invocation-contract.md +1 -1
- package/src/skf-export-skill/references/summary.md +1 -1
- package/src/skf-forger/SKILL.md +1 -0
- package/src/skf-forger/references/pipeline-mode.md +2 -2
- package/src/skf-quick-skill/SKILL.md +2 -1
- package/src/skf-quick-skill/references/finalize.md +1 -1
- package/src/skf-quick-skill/references/halt-contract.md +2 -2
- package/src/skf-quick-skill/references/write-and-validate.md +1 -1
- package/src/skf-refine-architecture/SKILL.md +2 -1
- package/src/skf-refine-architecture/customize.toml +1 -1
- package/src/skf-refine-architecture/references/report.md +1 -1
- package/src/skf-rename-skill/SKILL.md +1 -0
- package/src/skf-rename-skill/references/invocation-contract.md +2 -2
- package/src/skf-setup/SKILL.md +1 -0
- package/src/skf-setup/references/ccc-index.md +1 -1
- package/src/skf-setup/references/invocation-contract.md +1 -1
- package/src/skf-test-skill/SKILL.md +1 -0
- package/src/skf-test-skill/assets/output-section-formats.md +4 -4
- package/src/skf-test-skill/references/coherence-check.md +8 -6
- package/src/skf-test-skill/references/coverage-check-branches.md +23 -3
- package/src/skf-test-skill/references/coverage-check-tiers.md +3 -3
- package/src/skf-test-skill/references/coverage-check.md +30 -43
- package/src/skf-test-skill/references/init.md +1 -1
- package/src/skf-test-skill/references/inventory-contract.md +27 -0
- package/src/skf-test-skill/references/invocation-contract.md +3 -3
- package/src/skf-test-skill/references/report.md +3 -3
- package/src/skf-test-skill/references/scoring-rules.md +6 -6
- package/src/skf-test-skill/references/signature-comparison-rules.md +72 -0
- package/src/skf-test-skill/references/source-access-protocol.md +12 -8
- package/src/skf-test-skill/references/step-hard-gate.md +1 -0
- package/src/skf-test-skill/scripts/build-result-context.py +20 -3
- package/src/skf-test-skill/scripts/gap-ledger.py +78 -13
- package/src/skf-test-skill/scripts/load-coverage-inputs.py +345 -9
- package/src/skf-test-skill/scripts/locate-export-segments.py +591 -63
- package/src/skf-test-skill/scripts/score-signatures.py +939 -74
- package/src/skf-test-skill/scripts/validate-inventory.py +245 -29
- package/src/skf-update-skill/SKILL.md +1 -0
- package/src/skf-update-skill/references/gap-driven.md +28 -22
- package/src/skf-update-skill/references/invocation-contract.md +1 -1
- package/src/skf-update-skill/references/merge.md +4 -4
- package/src/skf-update-skill/references/report.md +1 -1
- package/src/skf-update-skill/references/write.md +5 -4
- package/src/skf-verify-stack/SKILL.md +2 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Signature Comparison Rules
|
|
2
|
+
|
|
3
|
+
Every signature judge compares by these rules: coverage-check.md §1's split-body cross-check, its §2 judge (or the main thread in its place), its §5b split-body Source (the Split-Body Sources section of coverage-check-branches.md), coverage-check-tiers.md's fallback per-file scan and coherence-check.md §4's reference judge (its `signature_match`). They decide what is a mismatch and how to tag one; each judge returns the JSON its step gives.
|
|
4
|
+
|
|
5
|
+
## Reading a form
|
|
6
|
+
|
|
7
|
+
A form claims only what it writes, and only what it claims is compared. Its parameter list claims each parameter it lists, in that order, and no other: a parameter it leaves out is one it says the export does not take (Difference tags say when that is `omitted-optional`). A type or a return type it does not write claims nothing (Annotations).
|
|
8
|
+
|
|
9
|
+
Three kinds of form do not claim a complete parameter list:
|
|
10
|
+
|
|
11
|
+
- A call or usage example passes values (literals, variables, `name=value`): `RecursiveSplitter()`, `embed(text, model=m)`, `<Canvas of={X} />`. It is compared as a call: each argument it passes must exist, by position or by name, and every required parameter must be passed; an optional parameter it leaves out is no difference. An empty call `X()` is one too, so it matches a source whose parameters are all optional. A component usage `<X a={x} b="y" />` reads as a list of bare names of the props it sets, and a call `f(a, b)` as its arguments in order, each the parameter at its position (or, written `name=value`, the one it names). A JSX spread (`{...props}`) or children can pass a required prop, and a call's `...args` a required parameter, so neither leaves one out. An entry's `usage_signature` written as a usage is read this way, and each usage in a list of usages is compared as a form of its own, under its entry's `form` index.
|
|
12
|
+
- An ellipsis (`...` or `…`) in a list stands for members it does not show and claims nothing about them: `(...)` and `(tasks, data, dataset, ...)` leave out no parameter.
|
|
13
|
+
- A list of bare names, with no type, default or `?` (`connect(host, port)`, `({ themes, defaultTheme })`), names parameters without claiming the list is complete: an optional member it leaves out is no difference, a required one is a mismatch.
|
|
14
|
+
|
|
15
|
+
A form that writes a type or a default for a parameter (`(date: Date) => string`) claims its list is complete: an optional parameter it leaves out is an `omitted-optional` difference (Difference tags).
|
|
16
|
+
|
|
17
|
+
Optionality is stated only by a default, a `?` (on the name, or as `?:`) or an optional marker: `optional` in a docstring-style list, a Required column, a Default cell that gives a value. A type never states it, and an empty Default cell states nothing. A list of parameters or props, or a table, that states optionality for none of its members states none, and optionality is not compared: `connect(host: str, port: int)` matches `def connect(host: str, port: int = 5432)`. Once a list states optionality for any member, each member it writes with no mark, unless it is rest or variadic, claims to be required; a Default cell and a destructuring default mark only their own member (Components). A default's value is not compared: it only marks its parameter optional.
|
|
18
|
+
|
|
19
|
+
A form that opens with text the skill states once for several names, then `; ` and the name's own members (inventory-contract.md), writes both: the members the shared text states are members that form writes, read before its own.
|
|
20
|
+
|
|
21
|
+
In a language without keyword arguments (TS/JS, Go, Rust, Java, C, C++), leaving out an optional parameter that comes before one the form writes shifts the caller's positions: that is a mismatch, not `omitted-optional` (`slice(str, end?)` against `(str, start?, end?)`).
|
|
22
|
+
|
|
23
|
+
The names of Python's `*args` and `**kwargs` are never compared (`**kwargs` against `**options` is no difference), and the parameters after `*` or `*args` are compared by name, not order. A positional-only `/` or keyword-only `*` marker the form leaves out is not compared, and one it adds where the source has none is no difference: calls that follow the form are valid.
|
|
24
|
+
|
|
25
|
+
The source's declaration decides: the documented form is compared with the parameters, the optionality and the annotations the signature declares, with one exception, a return annotation the body contradicts (Annotations).
|
|
26
|
+
|
|
27
|
+
In the split-body cross-check both forms are documented: each is read by these rules, a type, return type or type arguments one of them leaves out are not compared, and a parameter's optionality is compared only when both forms state it. An ellipsis claims nothing about the members it stands for, required ones included. A call, a list of bare names or a form its own file labels partial (each side's label is read from that side's file, not from the export's one `signature_form`) may leave out a parameter the other form does not state as required, unless leaving it out shifts a caller's positions (a language without keyword arguments, as above); any other form claims its list is complete, so leaving out a parameter the other writes is a mismatch (inventory-contract.md's examples). Each export's form is read alone: what the rows of other exports write, in the same table or file, states nothing about it, so `(action, sink, tracking_record, child_invalidation)` matches `child_invalidation: X | None = None` even when every other row of its table writes its defaults.
|
|
28
|
+
|
|
29
|
+
A name the skill documents in several forms (a brief usage in SKILL.md, a full props list in a reference file) comes with `alternatives`: coverage-check.md §2's check lists them after its `documented` form, and the fallback per-file scan's `documentedAlternatives` holds them by name. Compare the declaration with each form the check offers, each by its own reading rules, and report a mismatch for each form that does not match, with its index as `form` (0 is `documented`, 1 the first alternative), its `documented_sig` written as that form and its `difference` tagged against it. With `candidates`, a form matches when it matches any candidate, and its mismatch is reported at the closest candidate.
|
|
30
|
+
|
|
31
|
+
## Components
|
|
32
|
+
|
|
33
|
+
A component is an export the source declares as one: a function that takes one props object (plus a `ref`, for `forwardRef`) and returns JSX, an element or `null`, or a value the source declares as a component type, a wrapper's result, a class component, a `defineComponent` result or a Svelte component, in React, Preact, Solid, Vue or Svelte. A function with several positional parameters is a function, whatever it returns (`flexRender(Comp, props)`, `createPortal(children, container)`). A component's signature is its props `P`: a function component's props type, a class component's props type argument, a Vue component's `props` option or `defineProps` type or runtime object, or a Svelte component's props (`$props()`, or its `export let` declarations).
|
|
34
|
+
|
|
35
|
+
`FC<P>`, `React.FC<P>`, `FunctionComponent<P>`, `ComponentType<P>`, a `memo`, `forwardRef` or other higher-order component result over `P`, and `(props: P) => JSX.Element` (or `ReactElement`, `ReactNode` or `null`) are one contract, the props `P`: a component's return type and its wrapper are never compared. Callers set props by name, so their order is never compared.
|
|
36
|
+
|
|
37
|
+
A component's own props are the members written in its package: its interface or object type, object literals intersected into it, and members computed from its own code (`VariantProps<typeof buttonVariants>`). Members of a type from another package (`React.ComponentProps<"button">`, `HTMLAttributes<...>`, `ComponentProps<typeof Link>`) are inherited, as are the `ref` a `forwardRef` result takes and the optional `children` of `PropsWithChildren<P>`. A prop the form writes must exist on `P`, own or inherited. A component whose props type lives in a package outside the source tree (`const Tabs = TabsPrimitive.Root`) has its props left uncompared: only that it is a component is checked.
|
|
38
|
+
|
|
39
|
+
A prop the form writes that `P` lacks, or a type or an optionality it writes that the source contradicts, is a mismatch. So is a required own prop it leaves out, and a required inherited one (a `Link`'s `href`) left out of a full form. An optional own prop a typed props list or a table leaves out is an `omitted-optional` difference; one a list of bare names or a usage leaves out, and an optional inherited prop, is no difference. `| null` and `| undefined` on an optional prop are no difference (`VariantProps` adds both). A props type name the form writes that the source does not declare (`Button(props: ButtonProps)` against a source with no `ButtonProps`) is not compared by name, only the props the form lists. Values in a usage example (`variant="subtle"`) are not compared.
|
|
40
|
+
|
|
41
|
+
A props list states optionality as Reading a form says: `({ of, meta, story })`, or a list of names, states none, and in an object type literal (`{ of?: X; meta: M }`) that states it for any member, each member written with no mark claims to be required. A table states optionality by a Required column, a `?` on a prop's name or a Default cell that gives a value; its Type column states types, never optionality. A Default cell with a value marks only its own row optional and an empty one states nothing, so Create Skill's `| Prop | Type | Default | Description |` table never reads a prop with an empty Default cell as required. A destructuring default (`({ of, meta = {} })`) likewise marks only its own member, and the others state no optionality.
|
|
42
|
+
|
|
43
|
+
## Annotations
|
|
44
|
+
|
|
45
|
+
- A parameter written without a type is not compared on type: its name and order are, and its optionality as Reading a form says. Bare `*args` matches `*args: P.args`, and `**kwargs` matches `**kwargs: P.kwargs`.
|
|
46
|
+
- A return type the form does not state is not compared. A form states one when its entry has a non-blank `return_type` or the form writes one: after `->`, `=>`, `→` or `):`, or in its language's own position (Go's result list after the parameters, the type before the name in C, C++ and Java). A blank `return_type` (`""`) states none, so an entry with `params: ""` and no return type matches `def f() -> None`.
|
|
47
|
+
- A type the source leaves unannotated, on a parameter or a return, is not compared, except that a TS/JS parameter with a literal default has that literal's type: `pageSize = 20` is a `number`, so `pageSize?: string` is a mismatch. An unannotated return stays uncompared.
|
|
48
|
+
- A documented parameter type that names some of the members of the source's union is no mismatch, since a caller passing it makes a valid call (`schema: VectorSchemaProvider` against `VectorSchemaProvider | MultiVectorSchemaProvider`); a documented return type must cover every member the source can return.
|
|
49
|
+
- Type arguments the form leaves out are not compared: `dict` matches `dict[str, Any]`, and `list[dict]` matches `list[dict[str, Any]]`. Type arguments it writes are: `dict[str, int]` against `dict[str, str]` is a mismatch.
|
|
50
|
+
- A documented return type that matches what the body demonstrably returns, where the declared annotation says otherwise, is a mismatch tagged `annotation-contradicted` with `evidence_line`, the body line that returns that value: the skill documents what callers get, and the contradiction is the source's, which `score` reports without blocking. Against a declaration annotated `-> Callable[[], Callable[[], None]]` whose line 19 returns the function, `-> Callable[[], None]` is tagged with `evidence_line` 19. Tag it only when you can name that line: a body line that starts with `return`, or the declaration line itself when its own expression body follows its parameters there (`=> expr`, a Python `def ...: return x`). A Rust tail expression, a Kotlin or Scala expression body and a Ruby implicit return have no such line and cannot be tagged: there, as when you cannot name the line, the declaration decides. A judge whose result has no `difference` field (coherence-check.md §4's `signature_match`, coverage-check.md §5b's split-body Source) reads such a documented return type, at a line it can name, as agreeing with the source: a match for `signature_match`, and for §5b a form that agrees with the source. It holds for a return type alone, never a parameter's annotation.
|
|
51
|
+
|
|
52
|
+
## Equivalent spellings
|
|
53
|
+
|
|
54
|
+
Two spellings of one type are no mismatch:
|
|
55
|
+
|
|
56
|
+
- `Optional[X]`, `Union[X, None]` and `X | None`; `Union[A, B]` and `A | B`; a union's members in any order (`None | X`, `B | A`).
|
|
57
|
+
- `typing`'s `List`, `Dict`, `Tuple`, `Set`, `FrozenSet` and `Type` and the builtins `list`, `dict`, `tuple`, `set`, `frozenset` and `type`.
|
|
58
|
+
- A name with or without its `typing.`, `collections.abc.` or `React.` prefix: `React.ReactNode` and `ReactNode`.
|
|
59
|
+
- A quoted forward reference and the name it quotes: `"Node"` and `Node`.
|
|
60
|
+
- `Array<T>` and `T[]`; `ReadonlyArray<T>` and `readonly T[]`.
|
|
61
|
+
- A type alias the source declares and the type it names.
|
|
62
|
+
- Type variables renamed consistently: `(a: T) -> T` against `(a: U) -> U`, but not `(a: T, b: U)` against `(a: U, b: U)`.
|
|
63
|
+
- Type arguments the source leaves out are not compared either: the form's `dict[str, Any]` matches the source's bare `dict`.
|
|
64
|
+
- A union of callables that take the same parameters and one callable returning the union of their returns: `Callable[[A], X] | Callable[[A], Y]` and `(a: A) -> X | Y`.
|
|
65
|
+
|
|
66
|
+
## Overload sets
|
|
67
|
+
|
|
68
|
+
A declaration opening an overload set (Python `@overload` stubs, TS bodiless signatures) is read through its implementation: a documented signature matching the implementation or any overload is no mismatch, and so is one whose parameter types are, position by position, the unions of the overloads' types.
|
|
69
|
+
|
|
70
|
+
## Difference tags
|
|
71
|
+
|
|
72
|
+
A fact these rules leave uncompared is no difference. A mismatch carries `difference` when the two differ only in one of these ways, else no key: `parameter-name` (only parameter names differ: types, order, optionality and return type equal; a destructured property is part of a type), `omitted-optional` (the documented form leaves out only members the source marks optional) or `annotation-contradicted` (only the return type differs: `documented_sig` and `source_sig` write the same parameters, and the body returns the documented type at the mismatch's `evidence_line`, as Annotations says). A parameter or field with a default or a `?`, a TS/JS rest parameter, or a Python `*args` or `**kwargs`, is optional; a Rust `Option<T>` field a struct literal still requires is not. A renamed component prop is never `parameter-name`: callers set props by name, so it is a mismatch with no tag. Tag it in any language and form: `score` (coverage-check.md §2b) checks the facts. The example in coverage-check.md §2, tagged `omitted-optional`, stays Critical against a full form.
|
|
@@ -17,19 +17,19 @@ The clauses below decide which part of that surface is the coverage denominator.
|
|
|
17
17
|
- **A curated monorepo subset** (`--curated-subset`), where the extraction has no `counts.curated_subset` (Quick tier, the fallback per-file scan, States 2 to 4): `{source_is_monorepo}` is true and the brief's `scope.include` lists a curated subset rather than the full workspace.
|
|
18
18
|
- **The entry points' state** (`--barrel`), where no complete extraction or quick output gives it (the fallback per-file scan when the runner listed entry points, Quick tier read by eye, State 4): `none` when no barrel file is at any recognized entry-point path (`__init__.py`, `index.ts`/`index.js`, `lib.rs`, `mod.rs`), `empty` when the primary entry point exports nothing (`export {};`, an empty `__init__.py`, a `lib.rs` with no `pub use`), else `barrel`.
|
|
19
19
|
|
|
20
|
-
The recipe runner already re-derives each barrel (multi-line re-export blocks, star re-exports and a Rust `pub use` chain included) and counts each name once, however many in-scope files define it, so a barrel that only re-exports adds nothing of its own. Each of those files gives the name a surface record of its own (the runner's `entry_point_diff.other_definitions` beside its `public` row), so a set holds the name when any of its files matches, and the signature plan can compare the declaration the skill cites. Without the runner's names (Quick tier, the fallback per-file scan, States 2 to 4) the script builds the `scope.include` and `tier_a_include` sets from the files the names carry and the brief's globs (the runner's JSON the fallback scan passes adds only its scope and stale globs), and there is no `subpaths` or `root` set (coverage-check.md §2b). Never re-count a barrel by hand. A `scope.tier_a_include` glob counts a name only in a file a `scope.include` glob also matches, and one that matches no file (`guards.staleScope.unmatchedTierAInclude`, a brief older than the source) counts none: when the tier A globs count no name, `surface.json` has no `tier_a_include` set. Each clause, by its `denominator.clause`, with what stays yours:
|
|
20
|
+
The recipe runner already re-derives each barrel (multi-line re-export blocks, star re-exports and a Rust `pub use` chain included) and counts each name once, however many in-scope files define it, so a barrel that only re-exports adds nothing of its own. Each of those files gives the name a surface record of its own (the runner's `entry_point_diff.other_definitions` beside its `public` row), so a set holds the name when any of its files matches, and the signature plan can compare the declaration the skill cites. Without the runner's names (Quick tier, the fallback per-file scan, States 2 to 4) the script builds the `scope.include` and `tier_a_include` sets from the files the names carry and the brief's globs (the runner's JSON the fallback scan passes adds only its scope and stale globs), and there is no `subpaths` or `root` set (coverage-check.md §2b). Never re-count a barrel by hand. A `scope.tier_a_include` glob counts a name only in a file a `scope.include` glob also matches, and one that matches no file (`guards.staleScope.unmatchedTierAInclude`, a brief older than the source) counts none: when the tier A globs count no name, `surface.json` has no `tier_a_include` set. An annotation below takes each `{candidates.<field>}` count from `surface.json`, and a null one reads `absent`; a `*Files` count is the files that define the set's names (at Quick tier, word it as the files its names sit at). Each clause, by its `denominator.clause`, with what stays yours:
|
|
21
21
|
|
|
22
|
-
- **Empty-barrel packages (`empty-barrel`; copy-paste or subpath-only distribution):** the primary entry point is empty, so the denominator is the `scope.include` set. The brief's `scope.notes` should say the distribution model is by design; when it does, treat the empty barrel as intended rather than a bug. With no brief and an empty barrel, set `analysis_confidence: docs-only` and report that the source API surface could not be determined. Annotate: `Empty barrel, denominator: scope.include union ({
|
|
22
|
+
- **Empty-barrel packages (`empty-barrel`; copy-paste or subpath-only distribution):** the primary entry point is empty, so the denominator is the `scope.include` set. The brief's `scope.notes` should say the distribution model is by design; when it does, treat the empty barrel as intended rather than a bug. With no brief and an empty barrel, set `analysis_confidence: docs-only` and report that the source API surface could not be determined. Annotate: `Empty barrel, denominator: scope.include union ({candidates.scopeIncludeUnion} exports)`.
|
|
23
23
|
|
|
24
|
-
- **Stratified-scope monorepo packages (`stratified-scope`; curated subsets of multi-package repos):** the denominator reflects only the authored surface, not the monorepo's global export count: the `stats.effective_denominator` Create Skill stored, unless `guards.deflation.fires` (a Medium `metadata-drift` gap, coverage-check.md §4: a legitimate scope reduction goes in the brief, through `scope.tier_a_include` or `scope.exclude`, never into `metadata.stats`), then the `tier_a_include` set when the guard measured the stored count against it (`guards.deflation.basis` `tier_a_include`, in either direction: a stale count, which re-running Create Skill recounts, storing the runner's tier A count when `effective_denominator_decision.store` holds and `scope.notes` documents the stratification and otherwise dropping the stored count; on an incomplete extraction a set below the stored count leaves the stored count), else the `scope.include` set; with no stored count, the `tier_a_include` and `scope.include` sets. Never set `stats.effective_denominator` by hand: Update Skill's recount drops it. With no surface read from the source (`guards.deflation.applicable` false), annotate the report `effective_denominator used unverified: no source access to re-derive the barrel`. When `guards.inflation.fires` (the coarse globs sweep in internal infrastructure, a Medium `denominator-inflation` gap), the `scope.include` union is itself inflated: when the deflation guard fires too, that union stays the denominator and both gaps ask for a `scope.tier_a_include` list of the files that define the authored names (when `guards.umbrella.umbrella` is true, those files rather than the barrel). When the barrel branch counts that union, here or on the multi-entry ladder, the names it counts and the skill does not document are one more `denominator-inflation` gap beside the guard's at the brief, which defers to the guard's fix, not a `missing-export` gap each. Leave `analysis_confidence` unchanged. Annotate the report: `Stratified scope, denominator: {effective_denominator | tier_a_include union | scope.include union} ({
|
|
24
|
+
- **Stratified-scope monorepo packages (`stratified-scope`; curated subsets of multi-package repos):** the denominator reflects only the authored surface, not the monorepo's global export count: the `stats.effective_denominator` Create Skill stored, unless `guards.deflation.fires` (a Medium `metadata-drift` gap, coverage-check.md §4: a legitimate scope reduction goes in the brief, through `scope.tier_a_include` or `scope.exclude`, never into `metadata.stats`), then the `tier_a_include` set when the guard measured the stored count against it (`guards.deflation.basis` `tier_a_include`, in either direction: a stale count, which re-running Create Skill recounts, storing the runner's tier A count when `effective_denominator_decision.store` holds and `scope.notes` documents the stratification and otherwise dropping the stored count; on an incomplete extraction a set below the stored count leaves the stored count), else the `scope.include` set; with no stored count, the `tier_a_include` and `scope.include` sets. Never set `stats.effective_denominator` by hand: Update Skill's recount drops it. With no surface read from the source (`guards.deflation.applicable` false), annotate the report `effective_denominator used unverified: no source access to re-derive the barrel`. When `guards.inflation.fires` (the coarse globs sweep in internal infrastructure, a Medium `denominator-inflation` gap), the `scope.include` union is itself inflated: when the deflation guard fires too, that union stays the denominator and both gaps ask for a `scope.tier_a_include` list of the files that define the authored names (when `guards.umbrella.umbrella` is true, those files rather than the barrel). When the barrel branch counts that union, here or on the multi-entry ladder, the names it counts and the skill does not document are one more `denominator-inflation` gap beside the guard's at the brief, which defers to the guard's fix, not a `missing-export` gap each. Leave `analysis_confidence` unchanged. Annotate the report: `Stratified scope, denominator: {effective_denominator | tier_a_include union | scope.include union} ({candidates.tierAIncludeFiles | candidates.scopeIncludeFiles, by denominator.surfaceSet} files define its names, {candidates.tierAIncludeUnion | candidates.scopeIncludeUnion, by the same set} exports union)`; when `denominator.surfaceSet` is `all` no scope set gives a count, so both read `absent`. `scope.notes` should document the stratification (for example "Tier A: fully documented; Tier B: deferred to references; Tier C: excluded"); when it does, treat the curated subset as intended, not a scope gap.
|
|
25
25
|
|
|
26
26
|
- **Standard barrel (`standard-barrel`, the `all` set):** `full-library` skills, single-package repositories and briefs where the full monorepo is intentionally in scope. A `scope.include` glob that matches no file (a brief written for an older layout of the source) does not shrink that barrel: `guards.staleScope` restores the root exports defined in a file no glob covers, unless `scope.exclude` or a `scope-expansion` amendment whose latest action is `skipped` or `demoted-include` matches the file. The stale glob is a Medium `brief-scope-stale` gap (coverage-check.md §4), which also names each `scope.tier_a_include` glob that matches no file.
|
|
27
27
|
|
|
28
|
-
- **Pattern-reference apps (`pattern-reference`; non-library source):** a single-package repo that demonstrates an integration pattern rather than distributing a library API. The denominator is the `tier_a_include` set when `surface.json` has it, else the canonicalized provenance map, since `skf-create-skill`'s extraction already curated the map to the authored pattern surface. Leave `analysis_confidence` unchanged. Annotate: `Pattern-reference, denominator: {tier_a_include union | canonicalized provenance-map count} ({
|
|
28
|
+
- **Pattern-reference apps (`pattern-reference`; non-library source):** a single-package repo that demonstrates an integration pattern rather than distributing a library API. The denominator is the `tier_a_include` set when `surface.json` has it, else the canonicalized provenance map, since `skf-create-skill`'s extraction already curated the map to the authored pattern surface. Leave `analysis_confidence` unchanged. Annotate: `Pattern-reference, denominator: {tier_a_include union | canonicalized provenance-map count} ({candidates.tierAIncludeUnion | canonical.canonical, by the denominator} pattern surfaces)`, the canonicalized count being the `canonical.canonical` of the `surface.json` the `--fold` rerun wrote.
|
|
29
29
|
|
|
30
|
-
- **Single-crate curated subset (`specific-modules`):** a single-package repo whose brief carves a subset of the crate's public surface with `scope.include` and `scope.exclude`: the in-scope reachable barrel, or the `stats.effective_denominator` Create Skill stored for it, under the same deflation guard. Annotate: `Specific-modules subset, denominator: in-scope reachable barrel ({
|
|
30
|
+
- **Single-crate curated subset (`specific-modules`):** a single-package repo whose brief carves a subset of the crate's public surface with `scope.include` and `scope.exclude`: the in-scope reachable barrel, or the `stats.effective_denominator` Create Skill stored for it, under the same deflation guard. Annotate: `Specific-modules subset, denominator: in-scope reachable barrel ({candidates.scopeIncludeUnion} items from {candidates.scopeIncludeFiles} files, after scope.include/exclude filtering)`.
|
|
31
31
|
|
|
32
|
-
- **Multi-entry (exports-map) packages (`multi-entry`):** an in-scope `package.json` declares an `exports` map with more than one non-root subpath (`extraction.arms.multi_subpath_exports`). Installers reach every published subpath, so the denominator is the `subpaths` set (wildcard subpaths are left out: their surface is unbounded), or a stored count or `tier_a_include` set ahead of it, the stored count under the same deflation guard, whose fire counts the `tier_a_include` set when the surface has it, else the `scope.include` set. The root-barrel count (`candidates.rootBarrel`) is the secondary candidate in the Denominator Candidates block coverage-check.md §4 appends from coverage-check-branches.md. Annotate: `Multi-entry (exports-map), denominator: {effective_denominator | tier_a_include union | scope.include union | subpath union} ({
|
|
32
|
+
- **Multi-entry (exports-map) packages (`multi-entry`):** an in-scope `package.json` declares an `exports` map with more than one non-root subpath (`extraction.arms.multi_subpath_exports`). Installers reach every published subpath, so the denominator is the `subpaths` set (wildcard subpaths are left out: their surface is unbounded), or a stored count or `tier_a_include` set ahead of it, the stored count under the same deflation guard, whose fire counts the `tier_a_include` set when the surface has it, else the `scope.include` set. The root-barrel count (`candidates.rootBarrel`) is the secondary candidate in the Denominator Candidates block coverage-check.md §4 appends from coverage-check-branches.md. Annotate: `Multi-entry (exports-map), denominator: {effective_denominator | tier_a_include union | scope.include union | subpath union} ({candidates.subpathEntries} subpath entry points, {candidates.subpathUnion} exports union; root barrel: {candidates.rootBarrel})`.
|
|
33
33
|
|
|
34
34
|
Internal module symbols are **excluded** from the coverage denominator unless they are explicitly documented in SKILL.md (in which case they count as documented extras, not missing coverage): the surface holds only the names the entry points export, and the extraction's `internal` names never reach it. A documented extra reaches coverage-check.md §2c as a stale name. In a Python or TS/JS source, on a run where §2c runs `classify-stale` (the Forge, Forge+ or Deep tier, `analysis_confidence` `full`, `workspaceDrift` not `overridden` and a bound provenance map) and whose `surface.json` came from a complete extraction, it is an Info `observation` at the line where `classify-stale` finds the source still declares it (`defined_at`, or among several declaring files the one the skill cites on a line naming it), which update-skill never routes. Otherwise, and for a name a brief scopes out (`excluded.outsideScope`), it stays a Medium `stale-documentation` gap. When the brief is what scopes the file out (a `scope.exclude` glob matches it, or no `scope.include` glob covers it), the gap names that file and line and the glob: to keep the documentation, narrow the exclude glob (removing it takes a `demoted-exclude` amendment) or widen `scope.include`; else remove that documentation.
|
|
35
35
|
|
|
@@ -37,10 +37,14 @@ This matches the extraction-patterns.md convention used during skill creation: c
|
|
|
37
37
|
|
|
38
38
|
### Provenance-map canonicalization
|
|
39
39
|
|
|
40
|
-
A provenance map may hold **bookkeeping variants** of one export, artifacts of how the library structures its registry (Storybook's component-plus-story decomposition, accessibility renderer shadowing, exact-match renderer disambiguation). Counting them as separate exports inflates the denominator with names that are structural duplicates of a documented base export. When a clause takes its denominator from the provenance map, pass `--fold` to `surface`: it folds `X_def`, `X_exact` and `a11y_X` back to `X`, and `<prefix>X` back to `X` for each renderer prefix you pass as `--fold-prefix` (`mobile_`, `web_`, `ssr_`, only folded when `X` is an entry too), and reports the fold summary in `canonical.summary` for the report. Test-skill defines this fold convention: the base form is authoritative, and the variant is a sibling record.
|
|
40
|
+
A provenance map may hold **bookkeeping variants** of one export, artifacts of how the library structures its registry (Storybook's component-plus-story decomposition, accessibility renderer shadowing, exact-match renderer disambiguation). Counting them as separate exports inflates the denominator with names that are structural duplicates of a documented base export. When a clause takes its denominator from the provenance map, pass `--fold` to `surface`: it folds `X_def`, `X_exact` and `a11y_X` back to `X`, and `<prefix>X` back to `X` for each renderer prefix you pass as `--fold-prefix` (`mobile_`, `web_`, `ssr_`, only folded when `X` is an entry too), and reports the fold summary in `canonical.summary` for the report. Test-skill defines this fold convention: the base form is authoritative, and the variant is a sibling record. The provenance line check (coverage-check §4c) applies the same three rules, without `--fold-prefix` or `--keep`, when the cited file never names the variant: it checks the line against `X`.
|
|
41
41
|
|
|
42
42
|
A map whose raw count equals `stats.effective_denominator` is already canonical: `surface` folds nothing (`canonical.skipped`). A non-zero `canonical.residual` after folding (raw 222, canonical 215, `effective_denominator` 216) is an unexplained-reconciliation note; do not invent fold rules to close it. One judgment stays here: a variant the library's public surface genuinely distinguishes (an `a11y_Checkbox` documented and installed on its own) is a real export: pass it as `--keep`. Check SKILL.md for explicit documentation of the variant first; when in doubt, keep it and report both forms.
|
|
43
43
|
|
|
44
|
+
### What the Surface Holds
|
|
45
|
+
|
|
46
|
+
The surface holds the names the entry points export, less those defined in a file the brief scopes out (`excluded.outsideScope`); the recipes' internal matches are never part of it. A Python `__init__.py` nested in the folder of another package that exports a name is no barrel: its names (`excluded.nestedEntries`) leave only the `all` and `root` sets and the umbrella ratio, and `plan` still compares their signatures. When a `scope.include` glob matches no file (`guards.staleScope.fires`), the root exports defined in a file no glob covers and the brief does not exclude are back in it (`guards.staleScope.restored`). A stale `scope.tier_a_include` glob fires the guard too (`guards.staleScope.unmatchedTierAInclude`) and counts no name.
|
|
47
|
+
|
|
44
48
|
## Source Access Resolution
|
|
45
49
|
|
|
46
50
|
Before analysis, determine source access level. Walk through these states in order — use the first that succeeds:
|
|
@@ -64,7 +68,7 @@ If neither provenance-map nor metadata exports provide a usable baseline, but re
|
|
|
64
68
|
**State 5 — No source access at all:**
|
|
65
69
|
If none of the above succeed, fall through to docs-only mode (as defined in coverage-check.md Section 0: pre-analysis source type detection). Set `analysis_confidence: docs-only`. Warn: "**No source access available.** Coverage check evaluates documentation self-consistency only. Re-run with local clone or remote access for source-backed verification."
|
|
66
70
|
|
|
67
|
-
Set `analysis_confidence` in context for use in Section 2 analysis depth, step 5 output, and step 5 scoring. It names the source access only: tooling health is `toolingStatus`, which init.md records, and the two never overwrite each other.
|
|
71
|
+
Set `analysis_confidence` in context for use in Section 2 analysis depth, step 5 output, and step 5 scoring, and write it at once to the frontmatter of `{report_file}` (init.md §6c created it) as `analysisConfidence: '{analysis_confidence}'`, again each time a later rule changes it (an empty barrel with no brief makes it `docs-only`), so the hard gate and scoring find the value resolved here on disk, never one recalled after the context was compacted; step-hard-gate.md §3 and score.md §7 write the same value again. It names the source access only: tooling health is `toolingStatus`, which init.md records, and the two never overwrite each other.
|
|
68
72
|
|
|
69
73
|
**Confidence tier mapping:** `full` = T1, `provenance-map` = T1, `metadata-only` = T1-low, `remote-only` = T1-low, `docs-only` = T3. This aligns with the T1/T1-low/T2/T3 scale used across all SKF workflows.
|
|
70
74
|
|
|
@@ -51,6 +51,7 @@ The gate blocks the run, and scoring never runs. The run still ends through the
|
|
|
51
51
|
Update `{outputFile}` frontmatter:
|
|
52
52
|
- Set `hardGate: 'blocked'`
|
|
53
53
|
- Set `testResult: 'fail'`
|
|
54
|
+
- Set `analysisConfidence: '{analysis_confidence}'`: the source access coverage-check resolved, as score.md §7 writes it for a scored run
|
|
54
55
|
- Set `nextWorkflow: 'update-skill'`
|
|
55
56
|
- Append `'hard-gate'` to `stepsCompleted`
|
|
56
57
|
|
|
@@ -14,7 +14,10 @@ than the model retyping them from memory:
|
|
|
14
14
|
- the report frontmatter: skillName, runId, testMode, hardGate,
|
|
15
15
|
testResult, workspaceDrift, score, threshold, thresholdFallback,
|
|
16
16
|
originalThreshold and evidenceReportPath (the steps write the verdict
|
|
17
|
-
inputs there, so a compacted context cannot change them)
|
|
17
|
+
inputs there, so a compacted context cannot change them), and
|
|
18
|
+
analysisConfidence, which must be one of compute-score.py's
|
|
19
|
+
ANALYSIS_CONFIDENCE for a scored run and a blocked one alike (score.md
|
|
20
|
+
§7 writes it, or step-hard-gate.md §3 on a blocked run);
|
|
18
21
|
- the gap ledger: the gap counts by severity (gap-ledger.py's summary);
|
|
19
22
|
- the scoring script's output, which score.md saves: the verdict, score
|
|
20
23
|
and threshold it settled, activeCategories and inconclusiveReasons. A
|
|
@@ -82,8 +85,9 @@ takes: "stderr" for a blocked run, else "stdout".
|
|
|
82
85
|
Exit codes:
|
|
83
86
|
0 the payload was written
|
|
84
87
|
1 an input is missing or unreadable, the report's frontmatter holds no
|
|
85
|
-
settled verdict
|
|
86
|
-
|
|
88
|
+
settled verdict, an analysisConfidence that names no source access or
|
|
89
|
+
a verdict field the scoring output does not settle, or --output cannot
|
|
90
|
+
be written ({"status": "error", "error"} on stdout)
|
|
87
91
|
2 a usage error: no --score for a run the hard gate did not block (the
|
|
88
92
|
same JSON on stdout), or a flag argparse refuses (its usage on stderr)
|
|
89
93
|
"""
|
|
@@ -180,6 +184,11 @@ def _fallback_floor() -> int:
|
|
|
180
184
|
return _sibling("compute-score.py", "skf_compute_score_floor").FALLBACK_FLOOR
|
|
181
185
|
|
|
182
186
|
|
|
187
|
+
def _analysis_confidence() -> tuple[str, ...]:
|
|
188
|
+
"""The source access levels a report may record: compute-score.py's, which scores by them."""
|
|
189
|
+
return tuple(_sibling("compute-score.py", "skf_compute_score_confidence").ANALYSIS_CONFIDENCE)
|
|
190
|
+
|
|
191
|
+
|
|
183
192
|
def settled_test_result(scored: dict, workspace_drift: str | None) -> str | None:
|
|
184
193
|
"""The testResult the scoring output settles, or None when it holds no verdict.
|
|
185
194
|
|
|
@@ -241,6 +250,14 @@ def build(report: str, ledger: str, score: str | None = None, health_check: bool
|
|
|
241
250
|
except gl.LedgerError as exc:
|
|
242
251
|
raise ContextError(f"--ledger {ledger}: {exc}") from exc
|
|
243
252
|
|
|
253
|
+
# Every run records its source access, a blocked one included (step-hard-gate.md §3): a report missing it
|
|
254
|
+
# halts here, before report.md publishes it.
|
|
255
|
+
confidence = front.get("analysisConfidence")
|
|
256
|
+
levels = _analysis_confidence()
|
|
257
|
+
if confidence not in levels:
|
|
258
|
+
raise ContextError(f"--report {report} records no source access (analysisConfidence {confidence!r}, "
|
|
259
|
+
f"not one of: {', '.join(levels)})")
|
|
260
|
+
|
|
244
261
|
skill = front.get("skillName") or None
|
|
245
262
|
blocked = front.get("hardGate") == "blocked"
|
|
246
263
|
if blocked:
|
|
@@ -195,12 +195,21 @@ cites (never read; default `metadata.json`):
|
|
|
195
195
|
Create Skill, which rebuilds `exports[]` from the source.
|
|
196
196
|
provenance-line
|
|
197
197
|
--input skf-verify-provenance-completeness.py verify's result.
|
|
198
|
-
Each `stale[]` item
|
|
199
|
-
|
|
198
|
+
Each `stale[]` item, at its `{source_file}:{source_line}` with
|
|
199
|
+
`export`, by `reason`: `line-not-definition` is a Low
|
|
200
200
|
`provenance-line` gap `Provenance line is not the definition of
|
|
201
201
|
{export_name}` when `definition_lines` holds a line, else an Info
|
|
202
202
|
`provenance-unverified` gap `Provenance line not verified for
|
|
203
|
-
{export_name}
|
|
203
|
+
{export_name}`, and its issue names the base of a `folded_to`
|
|
204
|
+
variant; `file-missing` is a Low `provenance-line` gap
|
|
205
|
+
`Provenance file is missing for {export_name}`;
|
|
206
|
+
`line-out-of-bounds` a Low `provenance-line` gap `Provenance line
|
|
207
|
+
is out of range for {export_name}`; `line-invalid` an Info
|
|
208
|
+
`provenance-unverified` gap `Provenance line not verified for
|
|
209
|
+
{export_name}` at `{source_file}` alone. Its `missing`,
|
|
210
|
+
`orphaned` and `citations` are not gaps here, and no record is
|
|
211
|
+
appended for an export the ledger already records as a
|
|
212
|
+
`fabricated-signature` gap.
|
|
204
213
|
coherence --input aggregate-coherence.py's result. Each
|
|
205
214
|
`invalidReferences[]` entry at `SKILL.md:{line}`, by `status`:
|
|
206
215
|
`missing` is a Critical `broken-reference` gap, `inaccurate` a
|
|
@@ -308,8 +317,13 @@ CATEGORIES: dict[str, tuple[str, str]] = {
|
|
|
308
317
|
"stale-documentation": ("Coverage", "documentation for an export the enumerated source surface lacks"),
|
|
309
318
|
"missing-type": ("Coverage", "a type or interface the skill does not document"),
|
|
310
319
|
"provenance-completeness": ("Coverage", "an export the skill documents that the provenance map lacks"),
|
|
311
|
-
"provenance-line": (
|
|
312
|
-
|
|
320
|
+
"provenance-line": (
|
|
321
|
+
"Coverage",
|
|
322
|
+
"a provenance line that is not the definition of its export, or a citation whose file or line the source "
|
|
323
|
+
"lacks",
|
|
324
|
+
),
|
|
325
|
+
"provenance-unverified": (
|
|
326
|
+
"Coverage", "a provenance line the line-check rules could not verify, or one that is not a line number"),
|
|
313
327
|
"metadata-drift": ("Coverage", "export counts in the metadata that diverge from each other"),
|
|
314
328
|
"denominator-inflation": ("Coverage", "a scope include union larger than the provenance map"),
|
|
315
329
|
"brief-scope-stale": (
|
|
@@ -327,7 +341,10 @@ CATEGORIES: dict[str, tuple[str, str]] = {
|
|
|
327
341
|
"a reference whose real path leaves the skill, its source and, for a stack, the skills folder",
|
|
328
342
|
),
|
|
329
343
|
"integration-pattern": ("Coherence", "an integration pattern that is incomplete"),
|
|
330
|
-
"split-body-mismatch": (
|
|
344
|
+
"split-body-mismatch": (
|
|
345
|
+
"Coherence",
|
|
346
|
+
"two files of the skill (SKILL.md or a references/ file) that disagree about an export",
|
|
347
|
+
),
|
|
331
348
|
"scripts-assets": ("Coherence", "a Scripts & Assets section that is missing or names a missing file"),
|
|
332
349
|
"scripts-assets-provenance": ("Coherence", "a script or asset file without a provenance entry"),
|
|
333
350
|
"structural": (
|
|
@@ -1432,15 +1449,52 @@ def from_metadata_coherence(result: object, metadata: str | None = None) -> list
|
|
|
1432
1449
|
|
|
1433
1450
|
|
|
1434
1451
|
def from_provenance_line(result: object) -> list[dict]:
|
|
1435
|
-
"""verify's `
|
|
1452
|
+
"""verify's `stale[]` items: provenance line gaps, one per citation that does not resolve or is not the
|
|
1453
|
+
definition line."""
|
|
1436
1454
|
result = _object(result, "--input (skf-verify-provenance-completeness.py verify)", "stale")
|
|
1437
1455
|
records: list[dict] = []
|
|
1438
1456
|
for item in _list(result, "stale", "--input"):
|
|
1439
|
-
if not isinstance(item, dict)
|
|
1457
|
+
if not isinstance(item, dict):
|
|
1440
1458
|
continue
|
|
1459
|
+
reason = item.get("reason")
|
|
1441
1460
|
name, file, line = item.get("export_name"), item.get("source_file"), item.get("source_line")
|
|
1442
|
-
defs = [n for n in item.get("definition_lines") or [] if _count(n) is not None]
|
|
1443
1461
|
where = f"{file}:{line}"
|
|
1462
|
+
if reason == "file-missing":
|
|
1463
|
+
records.append(_gap(
|
|
1464
|
+
"Low", "provenance-line", f"Provenance file is missing for {name}", where,
|
|
1465
|
+
f"Set the provenance `source_file` and `source_line` of `{name}` to the file and line that define "
|
|
1466
|
+
"it in the pinned source, and move its citations there. update-skill `--from-test-report` does not "
|
|
1467
|
+
"repair this: it leaves the entry unchanged, with a warning that names the file.",
|
|
1468
|
+
issue=f"the provenance map records `{name}` at `{where}`, but the pinned source has no readable "
|
|
1469
|
+
f"file `{file}`; the export may live in another file, such as one with another extension.",
|
|
1470
|
+
export=name))
|
|
1471
|
+
continue
|
|
1472
|
+
if reason == "line-out-of-bounds":
|
|
1473
|
+
records.append(_gap(
|
|
1474
|
+
"Low", "provenance-line", f"Provenance line is out of range for {name}", where,
|
|
1475
|
+
f"Set the provenance `source_line` of `{name}` in `{file}` to the line that defines it and move "
|
|
1476
|
+
"its citations to that line; update-skill `--from-test-report` applies this when the file defines "
|
|
1477
|
+
"it on one line.",
|
|
1478
|
+
issue=f"the provenance map records `{name}` at `{where}`, which is not a line of `{file}` (past "
|
|
1479
|
+
"its end, or below 1).",
|
|
1480
|
+
export=name))
|
|
1481
|
+
continue
|
|
1482
|
+
if reason == "line-invalid": # no line number to cite: the Source is the file alone
|
|
1483
|
+
records.append(_gap(
|
|
1484
|
+
"Info", "provenance-unverified", f"Provenance line not verified for {name}", f"{file}",
|
|
1485
|
+
f"Set the provenance `source_line` of `{name}` in `{file}` to the number of the line that defines "
|
|
1486
|
+
"it.",
|
|
1487
|
+
issue=f"the provenance map records `{name}` in `{file}` at line `{line}`, which is not a line "
|
|
1488
|
+
"number, so no rule could check it; check by hand",
|
|
1489
|
+
export=name))
|
|
1490
|
+
continue
|
|
1491
|
+
if reason != "line-not-definition":
|
|
1492
|
+
continue
|
|
1493
|
+
defs = [n for n in item.get("definition_lines") or [] if _count(n) is not None]
|
|
1494
|
+
base = item.get("folded_to") if isinstance(item.get("folded_to"), str) and item.get("folded_to") else None
|
|
1495
|
+
# a bookkeeping variant the file never names was checked against its base (#847)
|
|
1496
|
+
folded = f" `{name}` is a bookkeeping variant `{file}` never names, checked against its base `{base}`." \
|
|
1497
|
+
if base else ""
|
|
1444
1498
|
if defs:
|
|
1445
1499
|
lines = ", ".join(str(n) for n in defs)
|
|
1446
1500
|
records.append(_gap(
|
|
@@ -1449,18 +1503,27 @@ def from_provenance_line(result: object) -> list[dict]:
|
|
|
1449
1503
|
"and move its citations to that line; update-skill `--from-test-report` applies this when the "
|
|
1450
1504
|
"file defines it on one line.",
|
|
1451
1505
|
issue=f"the provenance map records `{name}` at `{where}`, which is not the line that defines "
|
|
1452
|
-
f"it; the lines that define it are {lines}.",
|
|
1506
|
+
f"it; the lines that define it are {lines}.{folded}",
|
|
1453
1507
|
export=name))
|
|
1454
1508
|
else:
|
|
1455
1509
|
records.append(_gap(
|
|
1456
1510
|
"Info", "provenance-unverified", f"Provenance line not verified for {name}", where,
|
|
1457
|
-
f"Open `{file}` and confirm that line {line} defines `{name}`; if another line does, set
|
|
1458
|
-
"provenance `source_line` to it.",
|
|
1459
|
-
issue=f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand"
|
|
1511
|
+
f"Open `{file}` and confirm that line {line} defines `{base or name}`; if another line does, set "
|
|
1512
|
+
"the provenance `source_line` to it.",
|
|
1513
|
+
issue=f"the line-check rules found no definition line for `{name}` in `{file}`; check by hand"
|
|
1514
|
+
+ (f".{folded}" if folded else ""),
|
|
1460
1515
|
export=name))
|
|
1461
1516
|
return records
|
|
1462
1517
|
|
|
1463
1518
|
|
|
1519
|
+
def without_fabricated(ledger: dict, records: list[dict]) -> list[dict]:
|
|
1520
|
+
"""`--from provenance-line`'s records but those of an export the ledger already records as a
|
|
1521
|
+
`fabricated-signature` gap: coverage-check §5b appends that record first, and an export the source no longer
|
|
1522
|
+
holds takes no provenance line fix."""
|
|
1523
|
+
fabricated = {r.get("export") for r in ledger["records"] if r.get("category") == "fabricated-signature"}
|
|
1524
|
+
return [r for r in records if not r.get("export") or r.get("export") not in fabricated]
|
|
1525
|
+
|
|
1526
|
+
|
|
1464
1527
|
def from_coherence(result: object) -> list[dict]:
|
|
1465
1528
|
"""aggregate-coherence.py's invalid references."""
|
|
1466
1529
|
result = _object(result, "--input (aggregate-coherence.py)", "invalidReferences")
|
|
@@ -1756,6 +1819,8 @@ def _cmd_append(args: argparse.Namespace) -> int:
|
|
|
1756
1819
|
# one waits here instead of writing over the records it adds.
|
|
1757
1820
|
with ledger_lock(path):
|
|
1758
1821
|
ledger = load_ledger(path) if path.exists() else new_ledger(path)
|
|
1822
|
+
if args.adapter == "provenance-line":
|
|
1823
|
+
records = without_fabricated(ledger, records)
|
|
1759
1824
|
appended, duplicates = append_records(ledger, args.stage, records)
|
|
1760
1825
|
save_ledger(path, ledger)
|
|
1761
1826
|
except LedgerError as exc:
|