software-engineer — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited software-engineer (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
<skill_scope skill="software-engineer">
This skill provides fundamental software engineering philosophy and design principles derived from _Structure and Interpretation of Computer Programs_ (SICP) by Abelson and Sussman. Apply these principles to all software development work to create well-designed, maintainable, and robust systems.
Related skills:
functional-programmer — Functional paradigm principles and patternsobject-oriented-programmer — OOP principles including SOLIDlogic-programmer — Logic/relational programming patternstest-driven-development — Testing philosophy and practicesgit-version-control — Version control workflows</skill_scope>
<core_philosophy>
<program_as_expression>
Computer programming is fundamentally about expressing ideas, not just getting computers to do things. Code is written for humans to read and understand; execution by computers is incidental. Prioritize clarity and expressiveness in all code. </program_as_expression>
<utilitarian_code>
Code exists to solve problems, not to demonstrate cleverness. Write code that does what's right because it's right, not because it's an opportunity to show off language features or clever tricks.
Utilitarian principles:
The test: Would a tired engineer at 2am understand this code? If not, simplify it. </utilitarian_code>
<manage_complexity>
Complexity is the primary challenge in software engineering. Combat complexity through:
"Piles of kludges make a big complicated mess." — Gerald Jay Sussman
</manage_complexity>
<naming_and_scope>
Use naming and scope to control what parts of the program can see and affect other parts. Well-chosen names make programs self-documenting. Minimize scope to reduce coupling and increase modularity. </naming_and_scope>
<language_layers>
Complex systems benefit from layers of language, where each layer provides abstractions for the layer above. Design programs as a series of language levels, each suitable for expressing ideas at that level of abstraction.
"You can gain control of complexity by inventing new languages sometimes." — Hal Abelson
</language_layers>
<delayed_commitment>
Defer decisions about implementation details to maintain flexibility in discovering the right interfaces. The goal is not delay for its own sake, but rather:
The commitment being delayed is to internal representations and algorithms, not to understanding what makes a good interface. Use scaffolding and test-writing to explore interface designs before implementation. </delayed_commitment> </core_philosophy>
<code_as_ontology>
Public APIs are ontologies—systems of names carrying semantic commitments. This perspective, drawn from knowledge representation, shapes how we approach API design and system evolution.[^hickey-speculation]
<semantic_commitments>
When you name something in a public API, you make a promise about what that name means. User, createAccount, /api/users—these establish concepts in your system's ontology. Changing what a name means changes the ontology itself.
Design implications:
</semantic_commitments>
<open_world_assumption>
Design systems as if unknown facts might exist. From knowledge representation: what you don't know might be true, not merely false.
In practice: Data structures should tolerate unknown fields. Systems should ignore what they don't understand. Don't fail on unexpected information.
// Open-world: tolerates unknown fields
function processUser(user) {
const { id, name, email } = user;
// Ignores any other fields—system can evolve independently
return { id, processedName: name.toUpperCase(), email };
}Why this matters: Enables independent evolution of clients and servers. Adding capabilities doesn't break existing code. Systems compose safely. </open_world_assumption>
<accretion_framework>
Rich Hickey's accretion/breakage framework[^hickey-speculation] provides clear criteria for compatible changes:
Compatible changes (accretion):
Breaking changes:
# Accretion: add alongside, don't modify
def create_user(name, email): # Original—still works
...
def create_user_with_role(name, email, role): # New capability
...
# Or relax requirements:
def create_user(name, email, role=None): # Backward compatible
...Level independence: Changes at one level shouldn't force version changes at containing levels. Modifying one function doesn't change the module; modifying a module doesn't change the package. Version at the level where change occurs. </accretion_framework>
<semver_limitations>
One perspective[^hickey-speculation] on SemVer: major versions conflate all breaking changes into one signal. You know something broke, but not what or how badly. This makes major version bumps minimally informative.
Alternative approach: When incompatible changes are necessary, use new names rather than version numbers:
createUser → createUserV2users → users.v2my-lib → my-lib-nextNew names allow old and new to coexist, enabling gradual migration without forced upgrades. </semver_limitations>
<stability_enables_composition>
When names are enduring and changes accretive, systems compose safely:
Examples of enduring APIs: Unix system calls (1970s → today), Java core APIs (decades of backward compatibility), HTML (ancient pages still render), Maven Central (artifacts never removed or changed).
The pattern: an accreting collection of immutable artifacts where names mean what they've always meant. </stability_enables_composition>
<ontology_connection>
These principles aren't just good practice for general APIs—they're essential for semantic systems:
Ontology evolution: Vocabularies grow monotonically. Predicates don't change meaning; new predicates are added. Deprecation through annotation, not removal.
Schema evolution: Add types and properties; don't remove or redefine. New schemas extend old ones.
Query compatibility: Adding facts doesn't break existing queries. New data enriches results rather than invalidating them.
Inference stability: Adding axioms doesn't invalidate existing inferences. Monotonic reasoning preserves prior conclusions.
The discipline transfers both ways: Design software APIs as you would design an ontology—semantic commitments made carefully, growth through accretion, meaning preserved over time. Conversely, apply API evolution practices to ontology maintenance. </ontology_connection>
<ontological_design_checklist>
Names and Semantics:
Open-World Properties:
Evolution Strategy:
When breaking is necessary:
</ontological_design_checklist> </code_as_ontology>
<mixed_paradigm_thinking>
Effective software engineering requires choosing the right paradigm for each problem. Different paradigms excel at different tasks. For detailed guidance, see the paradigm-specific skills: functional-programmer, object-oriented-programmer, logic-programmer.
<when_functional>
Use functional approaches when:
Key principles: Pure functions, immutable data structures, first-class and higher-order functions, composition over inheritance. </when_functional>
<when_oop>
Use object-oriented approaches when:
Caution: Avoid overuse. Not every program needs objects. OOP can obscure functional clarity when misapplied.
Key principles: Encapsulation of state, clear interfaces and contracts, composition over inheritance, SOLID principles (see object-oriented-programmer skill). </when_oop>
<when_logic>
Use logic/relational approaches when:
Key principles: Relations over functions, unification and pattern matching, declarative specification, backtracking search. </when_logic>
<paradigm_integration>
Modern software often benefits from mixing paradigms:
Choose the paradigm that makes the problem clearest, not the paradigm most fashionable. </paradigm_integration> </mixed_paradigm_thinking>
<code_planning>
<design_iteration>
Design iteration is development work, not a delay before "real work." Do not jump to final implementation before understanding the problem space:
Design iteration is productive:
Prototyping as design exploration:
The distinction:
Indicators you need more design iteration:
Design exploration techniques:
When you're ready to scaffold production code:
Only after design iteration should you begin scaffolding production code. </design_iteration>
<scaffolding>
Plan code by writing scaffolds that can be filled out later:
When uncertain, ask the user—they are a senior software engineer who either has an answer or can find one. </scaffolding> </code_planning>
<design_principles>
<modularity>
Break programs into modules with clear responsibilities:
</modularity>
<testability>
Write code that is easy to test. For detailed testing philosophy, see test-driven-development skill.
</testability>
<progressive_enhancement>
Build systems incrementally:
"...the best way to make something is sometimes to just make it wrong first." — SuperfastMatt </progressive_enhancement>
<optimization>
Optimize for clarity first, performance second—but distinguish between speculation and expertise:
Avoid speculative optimization:
Apply expert knowledge:
The distinction: Premature optimization is optimizing based on speculation. Informed design decisions using expertise are not premature optimization—they're good engineering.
When optimizing:
</optimization> </design_principles>
<requirements_driven_design>
<user_needs>
Before designing solutions, understand and document what you're building and why:
Define user needs first:
Establish requirements from needs:
Trace everything back:
</user_needs>
<future_evolution>
Explicitly think about how the system might evolve:
Document future developments:
Design for evolution:
</future_evolution>
<success_criteria>
Establish how you'll know if the system serves its purpose:
Key Performance Indicators (KPIs):
Design for measurement:
</success_criteria>
<design_documentation>
Document design when:
Complexity warrants it:
Decisions need grounding:
Structure for design documents:
Use documentation to think:
</design_documentation>
<decision_grounding>
When evaluating design alternatives or making implementation decisions:
If you can't trace a decision back to user needs or requirements, either the decision is unnecessary or the user needs and requirements are incomplete. Investigate which is true—you may have identified a gap in requirements that should be documented. </decision_grounding> </requirements_driven_design>
<engineering_judgment>
<strategic_alignment>
Not everything that gets you from today to tomorrow also gets you from today to the day-after-tomorrow. Evaluate whether each decision advances toward ultimate goals, not just immediate milestones.
Path-dependent decisions:
Evaluate strategic alignment:
When immediate and long-term conflict:
Reference point for all decisions: The ultimate goals and end-state capabilities, not just the next feature or milestone. </strategic_alignment>
<context_over_dogma>
What works in one context (team size, domain, scale, constraints) may not work in another. Staff-level judgment requires:
The same approach that works at Google scale may be over-engineering at startup scale, and vice versa. </context_over_dogma>
<understanding_tradeoffs>
Every architectural and design decision involves trade-offs between competing concerns. There are no solutions, only trade-offs:
The goal is not to avoid trade-offs, but to make them consciously and document the reasoning. </understanding_tradeoffs>
<decision_reversibility>
Weight decisions differently based on the cost of reversibility. All decisions can be reversed if enough force is applied, but an expensive two-way door is effectively a one-way door.
Low-cost reversibility (move fast):
High-cost reversibility (move carefully):
For all decisions:
</decision_reversibility>
<systems_thinking>
Systems behave as wholes, not just as collections of parts. Staff-level engineers understand:
Design with the whole system in mind, not just individual components in isolation. </systems_thinking>
<abstraction_costs>
While this skill emphasizes abstraction as a complexity management tool, experienced engineers recognize when abstractions become counterproductive:
Abstractions are too expensive when they:
Consider removing abstractions when:
Sometimes the right answer is concrete, direct code rather than clever abstraction. </abstraction_costs>
<technical_debt>
Not all "technical debt" is bad—sometimes it's a deliberate, informed choice to ship faster:
Intentional technical debt (acceptable):
Accidental complexity (unacceptable):
Essential vs. accidental complexity (Brooks):
Managing technical debt:
</technical_debt>
<failure_resilience>
Assume components will fail; don't hope they won't. Resilient systems expect and handle failure:
Failure awareness:
Resilience patterns:
Design systems that continue functioning, perhaps with reduced capability, when components fail. </failure_resilience>
<observability>
Build systems that can be understood in production, not just in development:
Observability is not an afterthought:
Design for debuggability:
The question is not "does this work in my development environment" but "can we understand what's happening in production when things go wrong." </observability>
<architectural_patterns>
Complex systems benefit from architectural patterns that isolate business logic from infrastructure. These patterns prevent common problems but add complexity—apply them when benefits outweigh costs.
<hexagonal_architecture> #### Hexagonal Architecture (Ports & Adapters)
<hex_core_principle> Core principle: Isolate business logic from external systems (UI, database, APIs) through well-defined boundaries. All external interactions go through ports (interfaces) and adapters (implementations).
"Allow an application to equally be driven by users, programs, automated test or batch scripts, and to be developed and tested in isolation from its eventual run-time devices and databases." — Alistair Cockburn[^cockburn-hexarch]
</hex_core_principle>
<hex_structure> Structure:
</hex_structure>
<hex_warning_signs> Early warning signs you needed hexagonal architecture:
</hex_warning_signs>
<hex_when_to_use> When to recognize you need it early:
</hex_when_to_use>
<hex_tradeoffs> Trade-offs:
</hex_tradeoffs>
<hex_implementation> Implementation:
</hex_implementation>
<related_patterns> Origin and later formulations: Ports & Adapters is Alistair Cockburn's pattern, published in 2005.[^cockburn-hexarch] Two later architectures build on its separation, adding internal structure on the application side:
Works well with Domain-Driven Design:
</related_patterns>
<hex_when_simpler_suffices> When simpler approaches suffice:
</hex_when_simpler_suffices> </hexagonal_architecture> </architectural_patterns> </engineering_judgment>
<code_quality>
<naming_conventions>
Use names that reveal intent:
</naming_conventions>
<documentation_comments>
Document the why, not the what:
<comments_stand_without_authoring_context>)Documentation as context: Poor documentation is unacceptable. Strive for perfect documentation on all code entities, even tests. Address linter findings for documentation immediately to ensure documentation is written with proper context.
<comments_stand_without_authoring_context> #### Comments Stand Without the Authoring Context
Write every comment and doc comment so it reads correctly to someone holding only the current file: no earlier version of it, no diff, no issue thread, no memory of the discussion that produced the change. Assume the context you hold while editing is unavailable when the comment is read.
A comment that accumulates a record of successive edits duplicates what version control already holds, and holds less accurately.
Two readings of "why", one of them durable. "Why is this code like this?" can be answered with a fact about the edit ("we replaced the unbounded queue after the memory report came up") or with a fact about the code and the constraints it lives under ("an unbounded queue would exhaust heap when the producer bursts"). Write the second. The first holds only for a moment that has passed; the second holds as long as the code does, and it is what a reader needs in order to change the code safely.
Where each kind of material belongs. The following are examples, not a complete list:
| Material | Destination |
|---|---|
| What changed relative to a prior version | The commit message (see git-version-control skill, <conventional_commits>) |
| Alternatives weighed and rejected at design scale | A design document or decision record (see <design_documentation>) |
| The progression of a discussion, who asked for what, task or review status | No artifact — it is a property of the session, not of the code |
| A constraint that still binds the code's shape | A comment, in the present tense, naming a referent the reader can look up |
| Deprecation and version-introduced facts | The designated slot the language or doc tool provides (e.g., Javadoc's @since and @deprecated, Sphinx's .. deprecated:: — renamed .. version-deprecated:: in Sphinx 9.0, old name kept as an alias — or Swift's @available) rather than prose narration |
Rewrites. The following are examples, not a complete list:
| Instead of | Write |
|---|---|
// Now uses a bounded queue instead of the previous list | // Bounded so a bursting producer can't exhaust heap; capacity covers the 5s batch window |
/** Parses the config. Note: as of this change, also accepts TOML. */ | /** Parses the config from JSON, YAML, or TOML. */ |
// Per our discussion, validate here rather than in the handler | // Validate at the boundary so handlers can assume well-formed input |
// Dropped the retry loop because it was masking the real failure | (nothing — code that is no longer present needs no comment) |
// Sorts the keys, which fixes the flakiness from the earlier run | // Sorted for determinism; map iteration order varies per process |
A doc comment carries a contract for callers, so change history there competes with the contract for the reader's attention and goes stale against it:
# Narrates the edits
def load_config(path):
"""Load configuration.
Originally JSON-only; YAML and TOML support were added when the
deployment config moved over, and the strict= flag went away once
we agreed validation belonged in the caller.
"""
# States the contract, and keeps the durable reason
def load_config(path):
"""Load configuration from a JSON, YAML, or TOML file at ``path``.
Returns the parsed mapping without validating it. Callers validate,
so an invalid config can still be loaded for inspection.
"""When history is the constraint. Some comments reference the past because the past still binds the code: a field retained for wire compatibility with released clients, a workaround for an unfixed upstream bug, a shape imposed by an incomplete migration. Such a comment qualifies when both conditions hold — the constraint remains in force, and the comment names a referent the reader can pursue independently (e.g., a protocol version, a ticket, an upstream issue URL, a spec section). "Retained for clients on protocol v2" qualifies; "used to be a list" names nothing that still binds.
Check before committing. Read the comment and documentation lines the change adds or modifies (e.g., from git diff -U0) and ask of each whether it names something outside the code: a prior version, a conversation, a request, a review, a task, or a test run. Rewrite those as statements about the code as it stands; rewriting is the default. Drop a comment only when it describes something the code no longer contains, since there is then nothing left to state. Phrases worth pausing on include "previously", "used to", "no longer", "instead of", "as requested", "per our discussion", "we decided", "this fixes", and "this addresses". Delete commented-out code rather than annotating what it did; version control holds it. </comments_stand_without_authoring_context> </documentation_comments>
<error_handling>
Handle errors explicitly and meaningfully:
</error_handling>
<code_organization>
Organize code for human understanding:
</code_organization> </code_quality>
<working_with_existing_code>
<understanding_before_changing>
Before modifying code:
</understanding_before_changing>
<refactoring_strategy>
When refactoring:
<comments_stand_without_authoring_context>)</refactoring_strategy>
<legacy_code>
When working with legacy systems:
</legacy_code> </working_with_existing_code>
<language_tool_selection>
<choose_tools>
Select languages and tools based on:
</choose_tools>
<favor_existing>
Before writing new code:
Search the web for existing solutions when asked to write code—finding an existing library is preferable to writing new code. Clarify user expectations when unclear about whether they want a solution or actual code. </favor_existing> </language_tool_selection>
<principles_in_practice>
<iterative_development>
Develop software iteratively:
"...you don't completely know what you're doing 'till you've done it once." — Leo Brodie </iterative_development>
<practical_constraints>
When working on projects:
</practical_constraints>
<continuous_learning>
Software engineering requires continuous learning:
</continuous_learning>
<pragmatism_idealism>
Strike balance between:
The goal is sustainable software that serves its purpose well, not perfect code. </pragmatism_idealism> </principles_in_practice>
<safety_constraints>
These guardrails apply regardless of context or time pressure. They constrain toward safety, not away from good engineering.
<security_constraints>
</security_constraints>
<deployment_constraints>
</deployment_constraints>
<data_constraints>
</data_constraints>
<process_constraints>
</process_constraints> </safety_constraints>
<resources>
Primary texts:
Related skills:
functional-programmer — Functional paradigm principles and patternsobject-oriented-programmer — OOP principles including SOLIDlogic-programmer — Logic/relational programming patternstest-driven-development — Testing philosophy and practicesgit-version-control — Version control workflows</resources>
<sources> [^hickey-speculation]: Rich Hickey. 2016. Spec-ulation (keynote). Clojure/conj 2016. https://www.youtube.com/watch?v=oyLBGkS5ICk
[^cockburn-hexarch]: Alistair Cockburn. 2005. The Hexagonal (Ports & Adapters) Architecture. HaT Technical Report 2005.02, September 4, 2005. Retrieved July 29, 2026 from https://alistair.cockburn.us/hexagonal-architecture </sources>
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.